From 0e623e7ce46c8168e1ad0ee4f6015d2a825d784f Mon Sep 17 00:00:00 2001 From: kayone Date: Thu, 21 Nov 2013 21:26:57 -0800 Subject: [PATCH] imported signalr 1.1.3 into NzbDrone. --- src/Common/CodeAnalysisDictionary.xml | 22 + src/Common/CommonAssemblyInfo.cs | 20 + src/Common/CommonVersionInfo.cs | 7 + src/Common/GlobalSuppressions.cs | 18 + src/Common/Microsoft.AspNet.SignalR.ruleset | 227 ++++ src/Common/Microsoft.AspNet.SignalR.targets | 40 + .../AuthorizeAttribute.cs | 160 +++ .../Configuration/ConfigurationExtensions.cs | 61 + .../DefaultConfigurationManager.cs | 89 ++ .../Configuration/IConfigurationManager.cs | 33 + .../ConnectionConfiguration.cs | 24 + .../ConnectionExtensions.cs | 62 + .../ConnectionMessage.cs | 51 + src/Microsoft.AspNet.SignalR.Core/Cookie.cs | 28 + .../DefaultDependencyResolver.cs | 231 ++++ .../DependencyResolverExtensions.cs | 63 + .../GlobalHost.cs | 66 + .../GroupManager.cs | 117 ++ .../Hosting/HostConstants.cs | 26 + .../Hosting/HostContext.cs | 22 + .../Hosting/HostContextExtensions.cs | 53 + .../HostDependencyResolverExtensions.cs | 52 + .../Hosting/IResponse.cs | 45 + .../Hosting/IWebSocket.cs | 48 + .../Hosting/IWebSocketRequest.cs | 16 + .../Hosting/PersistentConnectionFactory.cs | 52 + .../Hosting/RequestExtensions.cs | 22 + .../Hosting/ResponseExtensions.cs | 33 + src/Microsoft.AspNet.SignalR.Core/Hub.cs | 75 ++ .../HubConfiguration.cs | 24 + .../Hubs/ClientHubInvocation.cs | 47 + .../Hubs/ClientProxy.cs | 44 + .../Hubs/ConnectionIdProxy.cs | 18 + .../Hubs/DefaultAssemblyLocator.cs | 16 + .../Hubs/DefaultHubActivator.cs | 32 + .../Hubs/DefaultJavaScriptProxyGenerator.cs | 211 ++++ .../Hubs/DynamicDictionary.cs | 144 +++ .../Hubs/EmptyJavaScriptProxyGenerator.cs | 15 + .../Hubs/Extensions/HubManagerExtensions.cs | 64 + .../Hubs/Extensions/HubTypeExtensions.cs | 30 + .../Hubs/Extensions/MethodExtensions.cs | 30 + .../Hubs/GroupProxy.cs | 18 + .../Hubs/HubCallerContext.cs | 71 ++ .../Hubs/HubConnectionContext.cs | 111 ++ .../Hubs/HubContext.cs | 66 + .../Hubs/HubDispatcher.cs | 522 ++++++++ .../Hubs/HubMethodNameAttribute.cs | 25 + .../Hubs/HubNameAttribute.cs | 25 + .../Hubs/HubRequest.cs | 20 + .../Hubs/HubRequestParser.cs | 69 + .../Hubs/HubResponse.cs | 45 + .../Hubs/IAssemblyLocator.cs | 14 + .../Hubs/IClientProxy.cs | 20 + .../Hubs/IHub.cs | 42 + .../Hubs/IHubActivator.cs | 9 + .../Hubs/IHubConnectionContext.cs | 17 + .../Hubs/IHubRequestParser.cs | 17 + .../Hubs/IJavaScriptMinifier.cs | 9 + .../Hubs/IJavaScriptProxyGenerator.cs | 9 + .../Hubs/Lookup/DefaultHubManager.cs | 96 ++ .../Hubs/Lookup/DefaultParameterResolver.cs | 54 + .../Hubs/Lookup/Descriptors/Descriptor.cs | 17 + .../Hubs/Lookup/Descriptors/HubDescriptor.cs | 22 + .../Lookup/Descriptors/MethodDescriptor.cs | 42 + .../Descriptors/NullMethodDescriptor.cs | 40 + .../Lookup/Descriptors/ParameterDescriptor.cs | 23 + .../Hubs/Lookup/HubMethodDispatcher.cs | 79 ++ .../Hubs/Lookup/IHubDescriptorProvider.cs | 28 + .../Hubs/Lookup/IHubManager.cs | 58 + .../Hubs/Lookup/IMethodDescriptorProvider.cs | 33 + .../Hubs/Lookup/IParameterResolver.cs | 22 + .../Lookup/ReflectedHubDescriptorProvider.cs | 87 ++ .../ReflectedMethodDescriptorProvider.cs | 150 +++ .../Hubs/NullClientProxy.cs | 21 + .../Hubs/NullJavaScriptMinifier.cs | 18 + .../Hubs/Pipeline/Auth/AuthorizeModule.cs | 125 ++ .../Pipeline/Auth/IAuthorizeHubConnection.cs | 18 + .../Auth/IAuthorizeHubMethodInvocation.cs | 18 + .../Pipeline/Auth/NotAuthorizedException.cs | 18 + .../Hubs/Pipeline/HubInvokerContext.cs | 42 + .../Pipeline/HubOutgoingInvokerContext.cs | 42 + .../Hubs/Pipeline/HubPipeline.cs | 103 ++ .../Hubs/Pipeline/HubPipelineExtensions.cs | 28 + .../Hubs/Pipeline/HubPipelineModule.cs | 311 +++++ .../Pipeline/IHubIncomingInvokerContext.cs | 34 + .../Pipeline/IHubOutgoingInvokerContext.cs | 34 + .../Hubs/Pipeline/IHubPipeline.cs | 29 + .../Hubs/Pipeline/IHubPipelineInvoker.cs | 76 ++ .../Hubs/Pipeline/IHubPipelineModule.cs | 87 ++ .../Hubs/ReflectionHelper.cs | 69 + .../Hubs/SignalProxy.cs | 63 + .../Hubs/StateChangeTracker.cs | 66 + .../Hubs/StatefulSignalProxy.cs | 48 + .../IConnection.cs | 24 + .../IConnectionGroupManager.cs | 21 + .../IDependencyResolver.cs | 15 + .../IGroupManager.cs | 28 + .../IHubContext.cs | 23 + .../IPersistentConnectionContext.cs | 22 + src/Microsoft.AspNet.SignalR.Core/IRequest.cs | 51 + .../Infrastructure/AckHandler.cs | 112 ++ .../Infrastructure/ArraySegmentTextReader.cs | 33 + .../Infrastructure/BufferTextWriter.cs | 192 +++ .../CancellationTokenExtensions.cs | 83 ++ .../Infrastructure/Connection.cs | 335 +++++ .../Infrastructure/ConnectionManager.cs | 113 ++ .../Infrastructure/DefaultProtectedData.cs | 35 + .../Infrastructure/DiffPair.cs | 20 + .../Infrastructure/DiffSet.cs | 65 + .../Infrastructure/DisposableAction.cs | 43 + .../Infrastructure/Disposer.cs | 62 + .../Infrastructure/ExceptionsExtensions.cs | 28 + .../Infrastructure/IAckHandler.cs | 15 + .../Infrastructure/IBinaryWriter.cs | 14 + .../Infrastructure/IConnectionManager.cs | 36 + .../Infrastructure/IPerformanceCounter.cs | 18 + .../IPerformanceCounterManager.cs | 194 +++ .../Infrastructure/IProtectedData.cs | 10 + .../Infrastructure/IServerCommandHandler.cs | 25 + .../Infrastructure/IServerIdManager.cs | 15 + .../Infrastructure/IStringMinifier.cs | 39 + .../Infrastructure/InterlockedHelper.cs | 16 + .../Infrastructure/ListHelper.cs | 12 + .../Infrastructure/NoOpPerformanceCounter.cs | 53 + .../PerformanceCounterAttribute.cs | 15 + .../PerformanceCounterManager.cs | 420 +++++++ .../PerformanceCounterWrapper.cs | 68 + .../PersistentConnectionContext.cs | 17 + .../Infrastructure/PrefixHelper.cs | 96 ++ .../Infrastructure/Purposes.cs | 11 + .../SafeCancellationTokenSource.cs | 104 ++ .../Infrastructure/SafeSet.cs | 60 + .../Infrastructure/ServerCommand.cs | 30 + .../Infrastructure/ServerCommandHandler.cs | 148 +++ .../Infrastructure/ServerCommandType.cs | 9 + .../Infrastructure/ServerIdManager.cs | 26 + .../SipHashBasedStringEqualityComparer.cs | 242 ++++ .../Infrastructure/StringMinifier.cs | 97 ++ .../Infrastructure/TaskQueue.cs | 131 ++ .../Json/IJsonSerializer.cs | 28 + .../Json/IJsonValue.cs | 26 + .../Json/IJsonWritable.cs | 18 + .../Json/JRawValue.cs | 42 + .../Json/JsonNetSerializer.cs | 70 ++ .../Json/JsonSerializerExtensions.cs | 77 ++ .../Json/JsonUtility.cs | 126 ++ .../Json/SipHashBasedDictionaryConverter.cs | 113 ++ .../Messaging/Command.cs | 20 + .../Messaging/CommandType.cs | 12 + .../Messaging/Cursor.cs | 252 ++++ .../Messaging/DefaultSubscription.cs | 219 ++++ .../Messaging/IMessageBus.cs | 29 + .../Messaging/ISubscriber.cs | 23 + .../Messaging/ISubscription.cs | 16 + .../Messaging/LocalEventKeyInfo.cs | 30 + .../Messaging/Message.cs | 155 +++ .../Messaging/MessageBroker.cs | 325 +++++ .../Messaging/MessageBus.cs | 588 +++++++++ .../Messaging/MessageBusExtensions.cs | 90 ++ .../Messaging/MessageResult.cs | 44 + .../Messaging/MessageStore.cs | 209 +++ .../Messaging/MessageStoreResult.cs | 53 + .../Messaging/ScaleoutConfiguration.cs | 39 + .../Messaging/ScaleoutMapping.cs | 37 + .../Messaging/ScaleoutMappingStore.cs | 124 ++ .../Messaging/ScaleoutMessage.cs | 75 ++ .../Messaging/ScaleoutMessageBus.cs | 233 ++++ .../Messaging/ScaleoutStore.cs | 441 +++++++ .../Messaging/ScaleoutStream.cs | 316 +++++ .../Messaging/ScaleoutStreamManager.cs | 98 ++ .../Messaging/ScaleoutSubscription.cs | 275 ++++ .../Messaging/Subscription.cs | 347 +++++ .../Messaging/Topic.cs | 119 ++ .../Messaging/TopicLookup.cs | 98 ++ .../Messaging/TopicState.cs | 12 + .../Messaging/Volatile.cs | 17 + .../Microsoft.AspNet.SignalR.Core.csproj | 279 ++++ ...oft.AspNet.SignalR.Core.csproj.DotSettings | 2 + .../PersistentConnection.cs | 496 ++++++++ .../Properties/AssemblyInfo.cs | 16 + .../Resources.Designer.cs | 378 ++++++ .../Resources.resx | 225 ++++ .../Scripts/hubs.js | 90 ++ .../TaskAsyncHelper.cs | 1119 +++++++++++++++++ .../Tracing/ITraceManager.cs | 12 + .../Tracing/TraceManager.cs | 31 + .../Tracing/TraceSourceExtensions.cs | 52 + .../Transports/ForeverFrameTransport.cs | 189 +++ .../Transports/ForeverTransport.cs | 405 ++++++ .../Transports/HttpRequestLifeTime.cs | 94 ++ .../Transports/ITrackingConnection.cs | 76 ++ .../Transports/ITransport.cs | 59 + .../Transports/ITransportConnection.cs | 15 + .../Transports/ITransportHeartBeat.cs | 38 + .../Transports/ITransportManager.cs | 25 + .../Transports/LongPollingTransport.cs | 401 ++++++ .../Transports/PersistentResponse.cs | 180 +++ .../Transports/ServerSentEventsTransport.cs | 93 ++ .../TransportConnectionExtensions.cs | 34 + .../Transports/TransportConnectionStates.cs | 19 + .../Transports/TransportDisconnectBase.cs | 336 +++++ .../Transports/TransportHeartBeat.cs | 384 ++++++ .../Transports/TransportManager.cs | 113 ++ .../Transports/WebSocketTransport.cs | 166 +++ .../packages.config | 5 + .../Handlers/CallHandler.cs | 99 ++ .../Handlers/HubDispatcherHandler.cs | 40 + .../Handlers/PersistentConnectionHandler.cs | 43 + .../Infrastructure/Headers.cs | 48 + .../Infrastructure/OwinConstants.cs | 46 + .../OwinEnvironmentExtensions.cs | 69 + .../Infrastructure/ParamDictionary.cs | 35 + .../Infrastructure/PrefixMatcher.cs | 50 + .../Infrastructure/UrlDecoder.cs | 150 +++ .../Microsoft.AspNet.SignalR.Owin.csproj | 106 ++ ...oft.AspNet.SignalR.Owin.csproj.DotSettings | 2 + .../OwinExtensions.cs | 86 ++ .../Properties/AssemblyInfo.cs | 6 + .../RequestExtensions.cs | 29 + .../Resources.Designer.cs | 99 ++ .../Resources.resx | 132 ++ .../ServerRequest.Owin.cs | 237 ++++ .../ServerRequest.cs | 156 +++ .../ServerResponse.cs | 78 ++ .../packages.config | 4 + src/NzbDrone.Api/packages.config | 1 - src/NzbDrone.Console/NzbDrone.Console.csproj | 16 +- src/NzbDrone.Console/packages.config | 2 - src/NzbDrone.Host/NzbDrone.Host.csproj | 16 +- src/NzbDrone.Host/packages.config | 2 - src/NzbDrone.SignalR/NzbDrone.SignalR.csproj | 7 +- src/NzbDrone.SignalR/packages.config | 1 - src/NzbDrone.sln | 29 + src/NzbDrone.sln.DotSettings | 16 + src/NzbDrone/NzbDrone.csproj | 16 +- src/NzbDrone/packages.config | 2 - 236 files changed, 20490 insertions(+), 35 deletions(-) create mode 100644 src/Common/CodeAnalysisDictionary.xml create mode 100644 src/Common/CommonAssemblyInfo.cs create mode 100644 src/Common/CommonVersionInfo.cs create mode 100644 src/Common/GlobalSuppressions.cs create mode 100644 src/Common/Microsoft.AspNet.SignalR.ruleset create mode 100644 src/Common/Microsoft.AspNet.SignalR.targets create mode 100644 src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Cookie.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/GroupManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hub.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IConnection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IHubContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/IRequest.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj create mode 100644 src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings create mode 100644 src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Resources.resx create mode 100644 src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js create mode 100644 src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs create mode 100644 src/Microsoft.AspNet.SignalR.Core/packages.config create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings create mode 100644 src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/Resources.resx create mode 100644 src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs create mode 100644 src/Microsoft.AspNet.SignalR.Owin/packages.config diff --git a/src/Common/CodeAnalysisDictionary.xml b/src/Common/CodeAnalysisDictionary.xml new file mode 100644 index 000000000..857f46c4e --- /dev/null +++ b/src/Common/CodeAnalysisDictionary.xml @@ -0,0 +1,22 @@ + + + + + Ack + Minifier + Jsonp + Linktionary + Scaleout + Redis + Owin + Stringify + Unminify + Unminified + Stateful + SignalR + Hubservable + Sse + GitHub + + + \ No newline at end of file diff --git a/src/Common/CommonAssemblyInfo.cs b/src/Common/CommonAssemblyInfo.cs new file mode 100644 index 000000000..c245672c6 --- /dev/null +++ b/src/Common/CommonAssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyCompany("Microsoft Open Technologies, Inc.")] +[assembly: AssemblyCopyright("© Microsoft Open Technologies, Inc. All rights reserved.")] + +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyConfiguration("")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] + +[assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/src/Common/CommonVersionInfo.cs b/src/Common/CommonVersionInfo.cs new file mode 100644 index 000000000..beed2ca20 --- /dev/null +++ b/src/Common/CommonVersionInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyVersion("1.1.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] +[assembly: AssemblyInformationalVersion("1.1.3")] diff --git a/src/Common/GlobalSuppressions.cs b/src/Common/GlobalSuppressions.cs new file mode 100644 index 000000000..ec9dfe318 --- /dev/null +++ b/src/Common/GlobalSuppressions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Code Analysis results, point to "Suppress Message", and click +// "In Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Strong naming is done on the CI.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "param", Scope = "resource", Target = "Microsoft.AspNet.SignalR.Resources.resources")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly", Justification = "We use semver")] +[assembly: SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Scope = "member", Target = "Microsoft.AspNet.SignalR.Messaging.ScaleoutTaskQueue.#.cctor()", Justification = "The task is cached")] diff --git a/src/Common/Microsoft.AspNet.SignalR.ruleset b/src/Common/Microsoft.AspNet.SignalR.ruleset new file mode 100644 index 000000000..38ad3e9ec --- /dev/null +++ b/src/Common/Microsoft.AspNet.SignalR.ruleset @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Common/Microsoft.AspNet.SignalR.targets b/src/Common/Microsoft.AspNet.SignalR.targets new file mode 100644 index 000000000..291b985a8 --- /dev/null +++ b/src/Common/Microsoft.AspNet.SignalR.targets @@ -0,0 +1,40 @@ + + + + $(ArtifactsDir)\$(MSBuildProjectName) + $(ArtifactsDir)\$(MSBuildProjectName)\bin + + + + $(MSBuildThisFileDirectory)Microsoft.AspNet.SignalR.ruleset + false + 1591 + true + + + + $(DefineConstants);CODE_ANALYSIS + 11.0 + + + + $(DefineConstants);MONO + + + + $(DefineConstants);SIGNED + true + true + $(KeyFile) + + + + + GlobalSuppressions.cs + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs new file mode 100644 index 000000000..82ef77896 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/AuthorizeAttribute.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Security.Principal; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Apply to Hubs and Hub methods to authorize client connections to Hubs and authorize client invocations of Hub methods. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "MVC and WebAPI don't seal their AuthorizeAttributes")] + public class AuthorizeAttribute : Attribute, IAuthorizeHubConnection, IAuthorizeHubMethodInvocation + { + private string _roles; + private string[] _rolesSplit = new string[0]; + private string _users; + private string[] _usersSplit = new string[0]; + + [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields", Justification = "Already somewhat represented by set-only RequiredOutgoing property.")] + protected bool? _requireOutgoing; + + /// + /// Set to false to apply authorization only to the invocations of any of the Hub's server-side methods. + /// This property only affects attributes applied to the Hub class. + /// This property cannot be read. + /// + [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Must be property because this is an attribute parameter.")] + public bool RequireOutgoing + { + // It is impossible to tell here whether the attribute is being applied to a method or class. This makes + // it impossible to determine whether the value should be true or false when _requireOutgoing is null. + // It is also impossible to have a Nullable attribute parameter type. + get { throw new NotImplementedException(Resources.Error_DoNotReadRequireOutgoing); } + set { _requireOutgoing = value; } + } + + /// + /// Gets or sets the user roles. + /// + public string Roles + { + get { return _roles ?? String.Empty; } + set + { + _roles = value; + _rolesSplit = SplitString(value); + } + } + + /// + /// Gets or sets the authorized users. + /// + public string Users + { + get { return _users ?? String.Empty; } + set + { + _users = value; + _usersSplit = SplitString(value); + } + } + + /// + /// Determines whether client is authorized to connect to . + /// + /// Description of the hub client is attempting to connect to. + /// The (re)connect request from the client. + /// true if the caller is authorized to connect to the hub; otherwise, false. + public virtual bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + // If RequireOutgoing is explicitly set to false, authorize all connections. + if (_requireOutgoing.HasValue && !_requireOutgoing.Value) + { + return true; + } + + return UserAuthorized(request.User); + } + + /// + /// Determines whether client is authorized to invoke the method. + /// + /// An providing details regarding the method invocation. + /// Indicates whether the interface instance is an attribute applied directly to a method. + /// true if the caller is authorized to invoke the method; otherwise, false. + public virtual bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod) + { + if (hubIncomingInvokerContext == null) + { + throw new ArgumentNullException("hubIncomingInvokerContext"); + } + + // It is impossible to require outgoing auth at the method level with SignalR's current design. + // Even though this isn't the stage at which outgoing auth would be applied, we want to throw a runtime error + // to indicate when the attribute is being used with obviously incorrect expectations. + + // We must explicitly check if _requireOutgoing is true since it is a Nullable type. + if (appliesToMethod && (_requireOutgoing == true)) + { + throw new ArgumentException(Resources.Error_MethodLevelOutgoingAuthorization); + } + + return UserAuthorized(hubIncomingInvokerContext.Hub.Context.User); + } + + /// + /// When overridden, provides an entry point for custom authorization checks. + /// Called by and . + /// + /// The for the client being authorize + /// true if the user is authorized, otherwise, false + protected virtual bool UserAuthorized(IPrincipal user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (!user.Identity.IsAuthenticated) + { + return false; + } + + if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) + { + return false; + } + + return true; + } + + private static string[] SplitString(string original) + { + if (String.IsNullOrEmpty(original)) + { + return new string[0]; + } + + var split = from piece in original.Split(',') + let trimmed = piece.Trim() + where !String.IsNullOrEmpty(trimmed) + select trimmed; + return split.ToArray(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs new file mode 100644 index 000000000..630fad897 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Configuration/ConfigurationExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Configuration +{ + internal static class ConfigurationExtensions + { + public const int MissedTimeoutsBeforeClientReconnect = 2; + public const int HeartBeatsPerKeepAlive = 2; + public const int HeartBeatsPerDisconnectTimeout = 6; + + /// + /// The amount of time the client should wait without seeing a keep alive before trying to reconnect. + /// + public static TimeSpan? KeepAliveTimeout(this IConfigurationManager config) + { + if (config.KeepAlive != null) + { + return TimeSpan.FromTicks(config.KeepAlive.Value.Ticks * MissedTimeoutsBeforeClientReconnect); + } + else + { + return null; + } + } + + /// + /// The interval between successively checking connection states. + /// + public static TimeSpan HeartbeatInterval(this IConfigurationManager config) + { + if (config.KeepAlive != null) + { + return TimeSpan.FromTicks(config.KeepAlive.Value.Ticks / HeartBeatsPerKeepAlive); + } + else + { + // If KeepAlives are disabled, have the heartbeat run at the same rate it would if the KeepAlive was + // kept at the default value. + return TimeSpan.FromTicks(config.DisconnectTimeout.Ticks / HeartBeatsPerDisconnectTimeout); + } + } + + /// + /// The amount of time a Topic should stay in memory after its last subscriber is removed. + /// + /// + /// + public static TimeSpan TopicTtl(this IConfigurationManager config) + { + // If the deep-alive is disabled, don't take it into account when calculating the topic TTL. + var keepAliveTimeout = config.KeepAliveTimeout() ?? TimeSpan.Zero; + + // Keep topics alive for twice as long as we let connections to reconnect. (The DisconnectTimeout) + // Also add twice the keep-alive timeout since clients might take a while to notice they are disconnected. + // This should be a very conservative estimate for how long we must wait before considering a topic dead. + return TimeSpan.FromTicks((config.DisconnectTimeout.Ticks + keepAliveTimeout.Ticks) * 2); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs new file mode 100644 index 000000000..af49f1978 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Configuration/DefaultConfigurationManager.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Configuration +{ + public class DefaultConfigurationManager : IConfigurationManager + { + // The below effectively sets the minimum heartbeat to once per second. + // if _minimumKeepAlive != 2 seconds, update the ArguementOutOfRanceExceptionMessage below + private static readonly TimeSpan _minimumKeepAlive = TimeSpan.FromSeconds(2); + + // if _minimumKeepAlivesPerDisconnectTimeout != 3, update the ArguementOutOfRanceExceptionMessage below + private const int _minimumKeepAlivesPerDisconnectTimeout = 3; + + // if _minimumDisconnectTimeout != 6 seconds, update the ArguementOutOfRanceExceptionMessage below + private static readonly TimeSpan _minimumDisconnectTimeout = TimeSpan.FromTicks(_minimumKeepAlive.Ticks * _minimumKeepAlivesPerDisconnectTimeout); + + private bool _keepAliveConfigured; + private TimeSpan? _keepAlive; + private TimeSpan _disconnectTimeout; + + public DefaultConfigurationManager() + { + ConnectionTimeout = TimeSpan.FromSeconds(110); + DisconnectTimeout = TimeSpan.FromSeconds(30); + DefaultMessageBufferSize = 1000; + } + + // TODO: Should we guard against negative TimeSpans here like everywhere else? + public TimeSpan ConnectionTimeout + { + get; + set; + } + + public TimeSpan DisconnectTimeout + { + get + { + return _disconnectTimeout; + } + set + { + if (value < _minimumDisconnectTimeout) + { + throw new ArgumentOutOfRangeException("value", Resources.Error_DisconnectTimeoutMustBeAtLeastSixSeconds); + } + + if (_keepAliveConfigured) + { + throw new InvalidOperationException(Resources.Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive); + } + + _disconnectTimeout = value; + _keepAlive = TimeSpan.FromTicks(_disconnectTimeout.Ticks / _minimumKeepAlivesPerDisconnectTimeout); + } + } + + public TimeSpan? KeepAlive + { + get + { + return _keepAlive; + } + set + { + if (value < _minimumKeepAlive) + { + throw new ArgumentOutOfRangeException("value", Resources.Error_KeepAliveMustBeGreaterThanTwoSeconds); + } + + if (value > TimeSpan.FromTicks(_disconnectTimeout.Ticks / _minimumKeepAlivesPerDisconnectTimeout)) + { + throw new ArgumentOutOfRangeException("value", Resources.Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout); + } + + _keepAlive = value; + _keepAliveConfigured = true; + } + } + + public int DefaultMessageBufferSize + { + get; + set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs b/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs new file mode 100644 index 000000000..9dd79a241 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Configuration/IConfigurationManager.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Configuration +{ + /// + /// Provides access to server configuration. + /// + public interface IConfigurationManager + { + /// + /// Gets or sets a representing the amount of time to leave a connection open before timing out. + /// + TimeSpan ConnectionTimeout { get; set; } + + /// + /// Gets or sets a representing the amount of time to wait after a connection goes away before raising the disconnect event. + /// + TimeSpan DisconnectTimeout { get; set; } + + /// + /// Gets or sets a representing the amount of time between send keep alive messages. + /// If enabled, this value must be at least two seconds. Set to null to disable. + /// + TimeSpan? KeepAlive { get; set; } + + /// + /// Gets of sets the number of messages to buffer for a specific signal. + /// + int DefaultMessageBufferSize { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs new file mode 100644 index 000000000..eef56241e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/ConnectionConfiguration.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR +{ + public class ConnectionConfiguration + { + // Resolver isn't set to GlobalHost.DependencyResolver in the ctor because it is lazily created. + private IDependencyResolver _resolver; + + /// + /// The dependency resolver to use for the hub connection. + /// + public IDependencyResolver Resolver + { + get { return _resolver ?? GlobalHost.DependencyResolver; } + set { _resolver = value; } + } + + /// + /// Determines if browsers can make cross domain requests to SignalR endpoints. + /// + public bool EnableCrossDomain { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs new file mode 100644 index 000000000..6f03e1c82 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/ConnectionExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR +{ + public static class ConnectionExtensions + { + /// + /// Sends a message to all connections subscribed to the specified signal. An example of signal may be a + /// specific connection id. + /// + /// The connection + /// The connectionId to send to. + /// The value to publish. + /// The list of connection ids to exclude + /// A task that represents when the broadcast is complete. + public static Task Send(this IConnection connection, string connectionId, object value, params string[] excludeConnectionIds) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + if (string.IsNullOrEmpty(connectionId)) + { + throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); + } + + var message = new ConnectionMessage(PrefixHelper.GetConnectionId(connectionId), + value, + PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); + + return connection.Send(message); + } + + /// + /// Broadcasts a value to all connections, excluding the connection ids specified. + /// + /// The connection + /// The value to broadcast. + /// The list of connection ids to exclude + /// A task that represents when the broadcast is complete. + public static Task Broadcast(this IConnection connection, object value, params string[] excludeConnectionIds) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + var message = new ConnectionMessage(connection.DefaultSignal, + value, + PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); + + return connection.Send(message); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs b/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs new file mode 100644 index 000000000..6c632c9f6 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/ConnectionMessage.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// A message sent to one more connections. + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Messags are never compared, just used as data.")] + public struct ConnectionMessage + { + /// + /// The signal to this message should be sent to. Connections subscribed to this signal + /// will receive the message payload. + /// + public string Signal { get; private set; } + + /// + /// The payload of the message. + /// + public object Value { get; private set; } + + /// + /// Represents a list of signals that should be used to filter what connections + /// receive this message. + /// + public IList ExcludedSignals { get; private set; } + + public ConnectionMessage(string signal, object value) + : this(signal, value, ListHelper.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The signal + /// The payload of the message + /// The signals to exclude. + public ConnectionMessage(string signal, object value, IList excludedSignals) + : this() + { + Signal = signal; + Value = value; + ExcludedSignals = excludedSignals; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Cookie.cs b/src/Microsoft.AspNet.SignalR.Core/Cookie.cs new file mode 100644 index 000000000..b251da93d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Cookie.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR +{ + public class Cookie + { + public Cookie(string name, string value) + : this(name, value, String.Empty, String.Empty) + { + + } + + public Cookie(string name, string value, string domain, string path) + { + Name = name; + Value = value; + Domain = domain; + Path = path; + } + + public string Name { get; private set; } + public string Domain { get; private set; } + public string Path { get; private set; } + public string Value { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs b/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs new file mode 100644 index 000000000..da8cb14fc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.SignalR.Configuration; +using Microsoft.AspNet.SignalR.Hubs; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; +using Microsoft.AspNet.SignalR.Tracing; +using Microsoft.AspNet.SignalR.Transports; + +namespace Microsoft.AspNet.SignalR +{ + public class DefaultDependencyResolver : IDependencyResolver + { + private readonly Dictionary>> _resolvers = new Dictionary>>(); + private readonly HashSet _trackedDisposables = new HashSet(); + private int _disposed; + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "It's easiest")] + public DefaultDependencyResolver() + { + RegisterDefaultServices(); + + // Hubs + RegisterHubExtensions(); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "The resolver is the class that does the most coupling by design.")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The resolver disposes dependencies on Dispose.")] + private void RegisterDefaultServices() + { + var traceManager = new Lazy(() => new TraceManager()); + Register(typeof(ITraceManager), () => traceManager.Value); + + var serverIdManager = new ServerIdManager(); + Register(typeof(IServerIdManager), () => serverIdManager); + + var serverMessageHandler = new Lazy(() => new ServerCommandHandler(this)); + Register(typeof(IServerCommandHandler), () => serverMessageHandler.Value); + + var newMessageBus = new Lazy(() => new MessageBus(this)); + Register(typeof(IMessageBus), () => newMessageBus.Value); + + var stringMinifier = new Lazy(() => new StringMinifier()); + Register(typeof(IStringMinifier), () => stringMinifier.Value); + + var serializer = new Lazy(); + Register(typeof(IJsonSerializer), () => serializer.Value); + + var transportManager = new Lazy(() => new TransportManager(this)); + Register(typeof(ITransportManager), () => transportManager.Value); + + var configurationManager = new DefaultConfigurationManager(); + Register(typeof(IConfigurationManager), () => configurationManager); + + var transportHeartbeat = new Lazy(() => new TransportHeartbeat(this)); + Register(typeof(ITransportHeartbeat), () => transportHeartbeat.Value); + + var connectionManager = new Lazy(() => new ConnectionManager(this)); + Register(typeof(IConnectionManager), () => connectionManager.Value); + + var ackHandler = new Lazy(); + Register(typeof(IAckHandler), () => ackHandler.Value); + + var perfCounterWriter = new Lazy(() => new PerformanceCounterManager(this)); + Register(typeof(IPerformanceCounterManager), () => perfCounterWriter.Value); + + var protectedData = new DefaultProtectedData(); + Register(typeof(IProtectedData), () => protectedData); + } + + private void RegisterHubExtensions() + { + var methodDescriptorProvider = new Lazy(); + Register(typeof(IMethodDescriptorProvider), () => methodDescriptorProvider.Value); + + var hubDescriptorProvider = new Lazy(() => new ReflectedHubDescriptorProvider(this)); + Register(typeof(IHubDescriptorProvider), () => hubDescriptorProvider.Value); + + var parameterBinder = new Lazy(); + Register(typeof(IParameterResolver), () => parameterBinder.Value); + + var activator = new Lazy(() => new DefaultHubActivator(this)); + Register(typeof(IHubActivator), () => activator.Value); + + var hubManager = new Lazy(() => new DefaultHubManager(this)); + Register(typeof(IHubManager), () => hubManager.Value); + + var proxyGenerator = new Lazy(() => new DefaultJavaScriptProxyGenerator(this)); + Register(typeof(IJavaScriptProxyGenerator), () => proxyGenerator.Value); + + var requestParser = new Lazy(); + Register(typeof(IHubRequestParser), () => requestParser.Value); + + var assemblyLocator = new Lazy(() => new DefaultAssemblyLocator()); + Register(typeof(IAssemblyLocator), () => assemblyLocator.Value); + + // Setup the default hub pipeline + var dispatcher = new Lazy(() => new HubPipeline().AddModule(new AuthorizeModule())); + Register(typeof(IHubPipeline), () => dispatcher.Value); + Register(typeof(IHubPipelineInvoker), () => dispatcher.Value); + } + + public virtual object GetService(Type serviceType) + { + if (serviceType == null) + { + throw new ArgumentNullException("serviceType"); + } + + IList> activators; + if (_resolvers.TryGetValue(serviceType, out activators)) + { + if (activators.Count == 0) + { + return null; + } + if (activators.Count > 1) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MultipleActivatorsAreaRegisteredCallGetServices, serviceType.FullName)); + } + return Track(activators[0]); + } + return null; + } + + public virtual IEnumerable GetServices(Type serviceType) + { + IList> activators; + if (_resolvers.TryGetValue(serviceType, out activators)) + { + if (activators.Count == 0) + { + return null; + } + return activators.Select(Track).ToList(); + } + return null; + } + + public virtual void Register(Type serviceType, Func activator) + { + IList> activators; + if (!_resolvers.TryGetValue(serviceType, out activators)) + { + activators = new List>(); + _resolvers.Add(serviceType, activators); + } + else + { + activators.Clear(); + } + activators.Add(activator); + } + + public virtual void Register(Type serviceType, IEnumerable> activators) + { + if (activators == null) + { + throw new ArgumentNullException("activators"); + } + + IList> list; + if (!_resolvers.TryGetValue(serviceType, out list)) + { + list = new List>(); + _resolvers.Add(serviceType, list); + } + else + { + list.Clear(); + } + foreach (var a in activators) + { + list.Add(a); + } + } + + private object Track(Func creator) + { + object obj = creator(); + + if (_disposed == 0) + { + var disposable = obj as IDisposable; + if (disposable != null) + { + lock (_trackedDisposables) + { + if (_disposed == 0) + { + _trackedDisposables.Add(disposable); + } + } + } + } + + return obj; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + lock (_trackedDisposables) + { + foreach (var d in _trackedDisposables) + { + d.Dispose(); + } + + _trackedDisposables.Clear(); + } + } + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs new file mode 100644 index 000000000..0aca7ed95 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/DependencyResolverExtensions.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR +{ + public static class DependencyResolverExtensions + { + public static T Resolve(this IDependencyResolver resolver) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + return (T)resolver.GetService(typeof(T)); + } + + public static object Resolve(this IDependencyResolver resolver, Type type) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + if (type == null) + { + throw new ArgumentNullException("type"); + } + + return resolver.GetService(type); + } + + public static IEnumerable ResolveAll(this IDependencyResolver resolver) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + return resolver.GetServices(typeof(T)).Cast(); + } + + public static IEnumerable ResolveAll(this IDependencyResolver resolver, Type type) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + if (type == null) + { + throw new ArgumentNullException("type"); + } + + return resolver.GetServices(type); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs b/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs new file mode 100644 index 000000000..7571495fc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/GlobalHost.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using Microsoft.AspNet.SignalR.Configuration; +using Microsoft.AspNet.SignalR.Hubs; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Provides access to default host information. + /// + public static class GlobalHost + { + private static readonly Lazy _defaultResolver = new Lazy(() => new DefaultDependencyResolver()); + private static IDependencyResolver _resolver; + + /// + /// Gets or sets the the default + /// + public static IDependencyResolver DependencyResolver + { + get + { + return _resolver ?? _defaultResolver.Value; + } + set + { + _resolver = value; + } + } + + /// + /// Gets the default + /// + public static IConfigurationManager Configuration + { + get + { + return DependencyResolver.Resolve(); + } + } + + /// + /// Gets the default + /// + public static IConnectionManager ConnectionManager + { + get + { + return DependencyResolver.Resolve(); + } + } + + /// + /// + /// + public static IHubPipeline HubPipeline + { + get + { + return DependencyResolver.Resolve(); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs new file mode 100644 index 000000000..c57476f70 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/GroupManager.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Messaging; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// The default implementation. + /// + public class GroupManager : IConnectionGroupManager + { + private readonly IConnection _connection; + private readonly string _groupPrefix; + + /// + /// Initializes a new instance of the class. + /// + /// The this group resides on. + /// The prefix for this group. Either a name or type name. + public GroupManager(IConnection connection, string groupPrefix) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + _connection = connection; + _groupPrefix = groupPrefix; + } + + /// + /// Sends a value to the specified group. + /// + /// The name of the group. + /// The value to send. + /// The list of connection ids to exclude + /// A task that represents when send is complete. + public Task Send(string groupName, object value, params string[] excludeConnectionIds) + { + if (string.IsNullOrEmpty(groupName)) + { + throw new ArgumentException((Resources.Error_ArgumentNullOrEmpty), "groupName"); + } + + var qualifiedName = CreateQualifiedName(groupName); + var message = new ConnectionMessage(qualifiedName, + value, + PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); + + return _connection.Send(message); + } + + /// + /// Adds a connection to the specified group. + /// + /// The connection id to add to the group. + /// The name of the group + /// A task that represents the connection id being added to the group. + public Task Add(string connectionId, string groupName) + { + if (connectionId == null) + { + throw new ArgumentNullException("connectionId"); + } + + if (groupName == null) + { + throw new ArgumentNullException("groupName"); + } + + var command = new Command + { + CommandType = CommandType.AddToGroup, + Value = CreateQualifiedName(groupName), + WaitForAck = true + }; + + return _connection.Send(connectionId, command); + } + + /// + /// Removes a connection from the specified group. + /// + /// The connection id to remove from the group. + /// The name of the group + /// A task that represents the connection id being removed from the group. + public Task Remove(string connectionId, string groupName) + { + if (connectionId == null) + { + throw new ArgumentNullException("connectionId"); + } + + if (groupName == null) + { + throw new ArgumentNullException("groupName"); + } + + var command = new Command + { + CommandType = CommandType.RemoveFromGroup, + Value = CreateQualifiedName(groupName), + WaitForAck = true + }; + + return _connection.Send(connectionId, command); + } + + private string CreateQualifiedName(string groupName) + { + return _groupPrefix + "." + groupName; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs new file mode 100644 index 000000000..9cb8b132a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostConstants.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hosting +{ + public static class HostConstants + { + /// + /// The host should set this if they need to enable debug mode + /// + public static readonly string DebugMode = "debugMode"; + + /// + /// The host should set this is web sockets can be supported + /// + public static readonly string SupportsWebSockets = "supportsWebSockets"; + + /// + /// The host should set this if the web socket url is different + /// + public static readonly string WebSocketServerUrl = "webSocketServerUrl"; + + public static readonly string ShutdownToken = "shutdownToken"; + + public static readonly string InstanceName = "instanceName"; + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs new file mode 100644 index 000000000..14135f1a1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Principal; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + public class HostContext + { + public IRequest Request { get; private set; } + public IResponse Response { get; private set; } + public IDictionary Items { get; private set; } + + public HostContext(IRequest request, IResponse response) + { + Request = request; + Response = response; + Items = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs new file mode 100644 index 000000000..d40d9e5ac --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostContextExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + public static class HostContextExtensions + { + public static T GetValue(this HostContext context, string key) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + object value; + if (context.Items.TryGetValue(key, out value)) + { + return (T)value; + } + return default(T); + } + + public static bool IsDebuggingEnabled(this HostContext context) + { + return context.GetValue(HostConstants.DebugMode); + } + + public static bool SupportsWebSockets(this HostContext context) + { + // The server needs to implement IWebSocketRequest for websockets to be supported. + // It also needs to set the flag in the items collection. + return context.GetValue(HostConstants.SupportsWebSockets) && + context.Request is IWebSocketRequest; + } + + public static string WebSocketServerUrl(this HostContext context) + { + return context.GetValue(HostConstants.WebSocketServerUrl); + } + + public static CancellationToken HostShutdownToken(this HostContext context) + { + return context.GetValue(HostConstants.ShutdownToken); + } + + public static string InstanceName(this HostContext context) + { + return context.GetValue(HostConstants.InstanceName); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs new file mode 100644 index 000000000..ce4f239f1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/HostDependencyResolverExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + public static class HostDependencyResolverExtensions + { + public static void InitializeHost(this IDependencyResolver resolver, string instanceName, CancellationToken hostShutdownToken) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + if (String.IsNullOrEmpty(instanceName)) + { + throw new ArgumentNullException("instanceName"); + } + + // Initialize the performance counters + resolver.InitializePerformanceCounters(instanceName, hostShutdownToken); + + // Dispose the dependency resolver on host shut down (cleanly) + resolver.InitializeResolverDispose(hostShutdownToken); + } + + private static void InitializePerformanceCounters(this IDependencyResolver resolver, string instanceName, CancellationToken hostShutdownToken) + { + var counters = resolver.Resolve(); + if (counters != null) + { + counters.Initialize(instanceName, hostShutdownToken); + } + } + + private static void InitializeResolverDispose(this IDependencyResolver resolver, CancellationToken hostShutdownToken) + { + // TODO: Guard against multiple calls to this + + // When the host triggers the shutdown token, dispose the resolver + hostShutdownToken.Register(state => + { + ((IDependencyResolver)state).Dispose(); + }, + resolver, + useSynchronizationContext: false); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs new file mode 100644 index 000000000..5c6121425 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/IResponse.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + /// + /// Represents a connection to the client. + /// + public interface IResponse + { + /// + /// Gets a cancellation token that represents the client's lifetime. + /// + CancellationToken CancellationToken { get; } + + /// + /// Gets or sets the content type of the response. + /// + string ContentType { get; set; } + + /// + /// Writes buffered data. + /// + /// The data to write to the buffer. + void Write(ArraySegment data); + + /// + /// Flushes the buffered response to the client. + /// + /// A task that represents when the data has been flushed. + Task Flush(); + + /// + /// Closes the connection to the client. + /// + /// A task that represents when the connection is closed. + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "End", Justification = "Ends the response thus the name is appropriate.")] + Task End(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs new file mode 100644 index 000000000..8db4362cc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocket.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + /// + /// Represents a web socket. + /// + public interface IWebSocket + { + /// + /// Invoked when data is sent over the websocket + /// + Action OnMessage { get; set; } + + /// + /// Invoked when the websocket gracefully closes + /// + Action OnClose { get; set; } + + /// + /// Invoked when there is an error + /// + Action OnError { get; set; } + + /// + /// Sends data over the websocket. + /// + /// The value to send. + /// A that represents the send is complete. + Task Send(string value); + + /// + /// Sends a chunk of data over the websocket ("endOfMessage" flag set to false.) + /// + /// + /// A that represents the send is complete. + Task SendChunk(ArraySegment message); + + /// + /// Sends a zero byte data chunk with the "endOfMessage" flag set to true. + /// + /// A that represents the flush is complete. + Task Flush(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs new file mode 100644 index 000000000..ce7109af0 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/IWebSocketRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + public interface IWebSocketRequest : IRequest + { + /// + /// Accepts an websocket request using the specified user function. + /// + /// The callback that fires when the websocket is ready. + Task AcceptWebSocketRequest(Func callback); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs new file mode 100644 index 000000000..33d9aeee1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/PersistentConnectionFactory.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Globalization; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + /// + /// Responsible for creating instances. + /// + public class PersistentConnectionFactory + { + private readonly IDependencyResolver _resolver; + + /// + /// Creates a new instance of the class. + /// + /// The dependency resolver to use for when creating the . + public PersistentConnectionFactory(IDependencyResolver resolver) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + _resolver = resolver; + } + + /// + /// Creates an instance of the specified type using the dependency resolver or the type's default constructor. + /// + /// The type of to create. + /// An instance of a . + public PersistentConnection CreateInstance(Type connectionType) + { + if (connectionType == null) + { + throw new ArgumentNullException("connectionType"); + } + + var connection = (_resolver.Resolve(connectionType) ?? + Activator.CreateInstance(connectionType)) as PersistentConnection; + + if (connection == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_IsNotA, connectionType.FullName, typeof(PersistentConnection).FullName)); + } + + return connection; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs new file mode 100644 index 000000000..52cba0096 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/RequestExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + internal static class RequestExtensions + { + /// + /// Gets a value from the QueryString, and if it's null or empty, gets it from the Form instead. + /// + public static string QueryStringOrForm(this IRequest request, string key) + { + var value = request.QueryString[key]; + if (String.IsNullOrEmpty(value)) + { + value = request.Form[key]; + } + return value; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs new file mode 100644 index 000000000..64e8cd7ca --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hosting/ResponseExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hosting +{ + /// + /// Extension methods for . + /// + public static class ResponseExtensions + { + /// + /// Closes the connection to a client with optional data. + /// + /// The . + /// The data to write to the connection. + /// A task that represents when the connection is closed. + public static Task End(this IResponse response, string data) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + + var bytes = Encoding.UTF8.GetBytes(data); + response.Write(new ArraySegment(bytes, 0, bytes.Length)); + return response.End(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hub.cs b/src/Microsoft.AspNet.SignalR.Core/Hub.cs new file mode 100644 index 000000000..8529aeca5 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hub.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Provides methods that communicate with SignalR connections that connected to a . + /// + public abstract class Hub : IHub + { + protected Hub() + { + Clients = new HubConnectionContext(); + Clients.All = new NullClientProxy(); + Clients.Others = new NullClientProxy(); + Clients.Caller = new NullClientProxy(); + } + + /// + /// + /// + public HubConnectionContext Clients { get; set; } + + /// + /// Provides information about the calling client. + /// + public HubCallerContext Context { get; set; } + + /// + /// The group manager for this hub instance. + /// + public IGroupManager Groups { get; set; } + + /// + /// Called when a connection disconnects from this hub instance. + /// + /// A + public virtual Task OnDisconnected() + { + return TaskAsyncHelper.Empty; + } + + /// + /// Called when the connection connects to this hub instance. + /// + /// A + public virtual Task OnConnected() + { + return TaskAsyncHelper.Empty; + } + + /// + /// Called when the connection reconnects to this hub instance. + /// + /// A + public virtual Task OnReconnected() + { + return TaskAsyncHelper.Empty; + } + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs new file mode 100644 index 000000000..13abdc5bc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/HubConfiguration.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR +{ + public class HubConfiguration : ConnectionConfiguration + { + /// + /// Determines whether JavaScript proxies for the server-side hubs should be auto generated at {Path}/hubs. + /// Defaults to true. + /// + public bool EnableJavaScriptProxies { get; set; } + + /// + /// Determines whether detailed exceptions thrown in Hub methods get reported back the invoking client. + /// Defaults to false. + /// + public bool EnableDetailedErrors { get; set; } + + public HubConfiguration() + { + EnableJavaScriptProxies = true; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs new file mode 100644 index 000000000..37dd52233 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientHubInvocation.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A description of a client-side hub method invocation. + /// + public class ClientHubInvocation + { + /// + /// The signal that clients receiving this invocation are subscribed to. + /// + [JsonIgnore] + public string Target { get; set; } + + /// + /// The name of the hub that the method being invoked belongs to. + /// + [JsonProperty("H")] + public string Hub { get; set; } + + /// + /// The name of the client-side hub method be invoked. + /// + [JsonProperty("M")] + public string Method { get; set; } + + /// + /// The argument list the client-side hub method will be called with. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Type is used for serialization.")] + [JsonProperty("A")] + public object[] Args { get; set; } + + /// + /// A key-value store representing the hub state on the server that has changed since the last time the hub + /// state was sent to the client. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Type is used for serialization.")] + [JsonProperty("S", NullValueHandling = NullValueHandling.Ignore)] + public IDictionary State { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs new file mode 100644 index 000000000..df87a13f1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/ClientProxy.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class ClientProxy : DynamicObject, IClientProxy + { + private readonly Func, Task> _send; + private readonly string _hubName; + private readonly IList _exclude; + + public ClientProxy(Func, Task> send, string hubName, IList exclude) + { + _send = send; + _hubName = hubName; + _exclude = exclude; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Binder is passed in by the DLR")] + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + result = Invoke(binder.Name, args); + return true; + } + + public Task Invoke(string method, params object[] args) + { + var invocation = new ClientHubInvocation + { + Hub = _hubName, + Method = method, + Args = args + }; + + return _send(PrefixHelper.GetHubName(_hubName), invocation, _exclude); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs new file mode 100644 index 000000000..cf3520fb8 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/ConnectionIdProxy.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class ConnectionIdProxy : SignalProxy + { + public ConnectionIdProxy(Func, Task> send, string signal, string hubName, params string[] exclude) : + base(send, signal, hubName, PrefixHelper.HubConnectionIdPrefix, exclude) + { + + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs new file mode 100644 index 000000000..b25b1985d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultAssemblyLocator.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DefaultAssemblyLocator : IAssemblyLocator + { + public virtual IList GetAssemblies() + { + return AppDomain.CurrentDomain.GetAssemblies(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs new file mode 100644 index 000000000..8f160d09a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultHubActivator.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DefaultHubActivator : IHubActivator + { + private readonly IDependencyResolver _resolver; + + public DefaultHubActivator(IDependencyResolver resolver) + { + _resolver = resolver; + } + + public IHub Create(HubDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException("descriptor"); + } + + if(descriptor.HubType == null) + { + return null; + } + + object hub = _resolver.Resolve(descriptor.HubType) ?? Activator.CreateInstance(descriptor.HubType); + return hub as IHub; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs new file mode 100644 index 000000000..7e84b318e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DefaultJavaScriptProxyGenerator : IJavaScriptProxyGenerator + { + private static readonly Lazy _templateFromResource = new Lazy(GetTemplateFromResource); + + private static readonly Type[] _numberTypes = new[] { typeof(byte), typeof(short), typeof(int), typeof(long), typeof(float), typeof(decimal), typeof(double) }; + private static readonly Type[] _dateTypes = new[] { typeof(DateTime), typeof(DateTimeOffset) }; + + private const string ScriptResource = "Microsoft.AspNet.SignalR.Scripts.hubs.js"; + + private readonly IHubManager _manager; + private readonly IJavaScriptMinifier _javaScriptMinifier; + private readonly Lazy _generatedTemplate; + + public DefaultJavaScriptProxyGenerator(IDependencyResolver resolver) : + this(resolver.Resolve(), + resolver.Resolve()) + { + } + + public DefaultJavaScriptProxyGenerator(IHubManager manager, IJavaScriptMinifier javaScriptMinifier) + { + _manager = manager; + _javaScriptMinifier = javaScriptMinifier ?? NullJavaScriptMinifier.Instance; + _generatedTemplate = new Lazy(() => GenerateProxy(_manager, _javaScriptMinifier, includeDocComments: false)); + } + + public string GenerateProxy(string serviceUrl) + { + serviceUrl = JavaScriptEncode(serviceUrl); + + var generateProxy = _generatedTemplate.Value; + + return generateProxy.Replace("{serviceUrl}", serviceUrl); + } + + public string GenerateProxy(string serviceUrl, bool includeDocComments) + { + serviceUrl = JavaScriptEncode(serviceUrl); + + string generateProxy = GenerateProxy(_manager, _javaScriptMinifier, includeDocComments); + + return generateProxy.Replace("{serviceUrl}", serviceUrl); + } + + private static string GenerateProxy(IHubManager hubManager, IJavaScriptMinifier javaScriptMinifier, bool includeDocComments) + { + string script = _templateFromResource.Value; + + var hubs = new StringBuilder(); + var first = true; + foreach (var descriptor in hubManager.GetHubs().OrderBy(h => h.Name)) + { + if (!first) + { + hubs.AppendLine(";"); + hubs.AppendLine(); + hubs.Append(" "); + } + GenerateType(hubManager, hubs, descriptor, includeDocComments); + first = false; + } + + if (hubs.Length > 0) + { + hubs.Append(";"); + } + + script = script.Replace("/*hubs*/", hubs.ToString()); + + return javaScriptMinifier.Minify(script); + } + + private static void GenerateType(IHubManager hubManager, StringBuilder sb, HubDescriptor descriptor, bool includeDocComments) + { + // Get only actions with minimum number of parameters. + var methods = GetMethods(hubManager, descriptor); + var hubName = GetDescriptorName(descriptor); + + sb.AppendFormat(" proxies.{0} = this.createHubProxy('{1}'); ", hubName, hubName).AppendLine(); + sb.AppendFormat(" proxies.{0}.client = {{ }};", hubName).AppendLine(); + sb.AppendFormat(" proxies.{0}.server = {{", hubName); + + bool first = true; + + foreach (var method in methods) + { + if (!first) + { + sb.Append(",").AppendLine(); + } + GenerateMethod(sb, method, includeDocComments, hubName); + first = false; + } + sb.AppendLine(); + sb.Append(" }"); + } + + private static string GetDescriptorName(Descriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException("descriptor"); + } + + string name = descriptor.Name; + + // If the name was not specified then do not camel case + if (!descriptor.NameSpecified) + { + name = JsonUtility.CamelCase(name); + } + + return name; + } + + private static IEnumerable GetMethods(IHubManager manager, HubDescriptor descriptor) + { + return from method in manager.GetHubMethods(descriptor.Name) + group method by method.Name into overloads + let oload = (from overload in overloads + orderby overload.Parameters.Count + select overload).FirstOrDefault() + orderby oload.Name + select oload; + } + + private static void GenerateMethod(StringBuilder sb, MethodDescriptor method, bool includeDocComments, string hubName) + { + var parameterNames = method.Parameters.Select(p => p.Name).ToList(); + sb.AppendLine(); + sb.AppendFormat(" {0}: function ({1}) {{", GetDescriptorName(method), Commas(parameterNames)).AppendLine(); + if (includeDocComments) + { + sb.AppendFormat(Resources.DynamicComment_CallsMethodOnServerSideDeferredPromise, method.Name, method.Hub.Name).AppendLine(); + var parameterDoc = method.Parameters.Select(p => String.Format(CultureInfo.CurrentCulture, Resources.DynamicComment_ServerSideTypeIs, p.Name, MapToJavaScriptType(p.ParameterType), p.ParameterType)).ToList(); + if (parameterDoc.Any()) + { + sb.AppendLine(String.Join(Environment.NewLine, parameterDoc)); + } + } + sb.AppendFormat(" return proxies.{0}.invoke.apply(proxies.{0}, $.merge([\"{1}\"], $.makeArray(arguments)));", hubName, method.Name).AppendLine(); + sb.Append(" }"); + } + + private static string MapToJavaScriptType(Type type) + { + if (!type.IsPrimitive && !(type == typeof(string))) + { + return "Object"; + } + if (type == typeof(string)) + { + return "String"; + } + if (_numberTypes.Contains(type)) + { + return "Number"; + } + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return "Array"; + } + if (_dateTypes.Contains(type)) + { + return "Date"; + } + return String.Empty; + } + + private static string Commas(IEnumerable values) + { + return Commas(values, v => v); + } + + private static string Commas(IEnumerable values, Func selector) + { + return String.Join(", ", values.Select(selector)); + } + + private static string GetTemplateFromResource() + { + using (Stream resourceStream = typeof(DefaultJavaScriptProxyGenerator).Assembly.GetManifestResourceStream(ScriptResource)) + { + var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } + } + + private static string JavaScriptEncode(string value) + { + value = JsonConvert.SerializeObject(value); + + // Remove the quotes + return value.Substring(1, value.Length - 2); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs new file mode 100644 index 000000000..0a9963108 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/DynamicDictionary.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DynamicDictionary : DynamicObject, IDictionary + { + private readonly IDictionary _obj; + + public DynamicDictionary(IDictionary obj) + { + _obj = obj; + } + + public object this[string key] + { + get + { + object result; + _obj.TryGetValue(key, out result); + return Wrap(result); + } + set + { + _obj[key] = Unwrap(value); + } + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = this[binder.Name]; + return true; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] + public override bool TrySetMember(SetMemberBinder binder, object value) + { + this[binder.Name] = value; + return true; + } + + public static object Wrap(object value) + { + var obj = value as IDictionary; + if (obj != null) + { + return new DynamicDictionary(obj); + } + + return value; + } + + public static object Unwrap(object value) + { + var dictWrapper = value as DynamicDictionary; + if (dictWrapper != null) + { + return dictWrapper._obj; + } + + return value; + } + + public void Add(string key, object value) + { + _obj.Add(key, value); + } + + public bool ContainsKey(string key) + { + return _obj.ContainsKey(key); + } + + public ICollection Keys + { + get { return _obj.Keys; } + } + + public bool Remove(string key) + { + return _obj.Remove(key); + } + + public bool TryGetValue(string key, out object value) + { + return _obj.TryGetValue(key, out value); + } + + public ICollection Values + { + get { return _obj.Values; } + } + + public void Add(KeyValuePair item) + { + _obj.Add(item); + } + + public void Clear() + { + _obj.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return _obj.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _obj.CopyTo(array, arrayIndex); + } + + public int Count + { + get { return _obj.Count; } + } + + public bool IsReadOnly + { + get { return _obj.IsReadOnly; } + } + + public bool Remove(KeyValuePair item) + { + return _obj.Remove(item); + } + + public IEnumerator> GetEnumerator() + { + return _obj.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs new file mode 100644 index 000000000..d0ed9055e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/EmptyJavaScriptProxyGenerator.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Globalization; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class EmptyJavaScriptProxyGenerator : IJavaScriptProxyGenerator + { + public string GenerateProxy(string serviceUrl) + { + return String.Format(CultureInfo.InvariantCulture, "throw new Error('{0}');", Resources.Error_JavaScriptProxyDisabled); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs new file mode 100644 index 000000000..570ea82a0 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubManagerExtensions.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public static class HubManagerExtensions + { + public static HubDescriptor EnsureHub(this IHubManager hubManager, string hubName, params IPerformanceCounter[] counters) + { + if (hubManager == null) + { + throw new ArgumentNullException("hubManager"); + } + + if (String.IsNullOrEmpty(hubName)) + { + throw new ArgumentNullException("hubName"); + } + + if (counters == null) + { + throw new ArgumentNullException("counters"); + } + + var descriptor = hubManager.GetHub(hubName); + + if (descriptor == null) + { + for (var i = 0; i < counters.Length; i++) + { + counters[i].Increment(); + } + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_HubCouldNotBeResolved, hubName)); + } + + return descriptor; + } + + public static IEnumerable GetHubs(this IHubManager hubManager) + { + if (hubManager == null) + { + throw new ArgumentNullException("hubManager"); + } + + return hubManager.GetHubs(d => true); + } + + public static IEnumerable GetHubMethods(this IHubManager hubManager, string hubName) + { + if (hubManager == null) + { + throw new ArgumentNullException("hubManager"); + } + + return hubManager.GetHubMethods(hubName, m => true); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs new file mode 100644 index 000000000..1bf73da42 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/HubTypeExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal static class HubTypeExtensions + { + internal static string GetHubName(this Type type) + { + if (!typeof(IHub).IsAssignableFrom(type)) + { + return null; + } + + return GetHubAttributeName(type) ?? type.Name; + } + + internal static string GetHubAttributeName(this Type type) + { + if (!typeof(IHub).IsAssignableFrom(type)) + { + return null; + } + + // We can still return null if there is no attribute name + return ReflectionHelper.GetAttributeValue(type, attr => attr.HubName); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs new file mode 100644 index 000000000..f1a57c2e8 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Extensions/MethodExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public static class MethodExtensions + { + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Justification = "The condition checks for null parameters")] + public static bool Matches(this MethodDescriptor methodDescriptor, IList parameters) + { + if (methodDescriptor == null) + { + throw new ArgumentNullException("methodDescriptor"); + } + + if ((methodDescriptor.Parameters.Count > 0 && parameters == null) + || methodDescriptor.Parameters.Count != parameters.Count) + { + return false; + } + + return true; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs new file mode 100644 index 000000000..9ca86d5ce --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/GroupProxy.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class GroupProxy : SignalProxy + { + public GroupProxy(Func, Task> send, string signal, string hubName, IList exclude) : + base(send, signal, hubName, PrefixHelper.HubGroupPrefix, exclude) + { + + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs new file mode 100644 index 000000000..2413da7da --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubCallerContext.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class HubCallerContext + { + /// + /// Gets the connection id of the calling client. + /// + public string ConnectionId { get; private set; } + + /// + /// Gets the cookies for the request. + /// + public IDictionary RequestCookies + { + get + { + return Request.Cookies; + } + } + + /// + /// Gets the headers for the request. + /// + public NameValueCollection Headers + { + get + { + return Request.Headers; + } + } + + /// + /// Gets the querystring for the request. + /// + public NameValueCollection QueryString + { + get + { + return Request.QueryString; + } + } + + /// + /// Gets the for the request. + /// + public IPrincipal User + { + get + { + return Request.User; + } + } + + /// + /// Gets the for the current HTTP request. + /// + public IRequest Request { get; private set; } + + public HubCallerContext(IRequest request, string connectionId) + { + ConnectionId = connectionId; + Request = request; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs new file mode 100644 index 000000000..fded4874b --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubConnectionContext.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Encapsulates all information about an individual SignalR connection for an . + /// + public class HubConnectionContext : IHubConnectionContext + { + private readonly string _hubName; + private readonly string _connectionId; + private readonly Func, Task> _send; + + /// + /// Initializes a new instance of the . + /// + public HubConnectionContext() + { + } + + /// + /// Initializes a new instance of the . + /// + /// The pipeline invoker. + /// The connection. + /// The hub name. + /// The connection id. + /// The connection hub state. + public HubConnectionContext(IHubPipelineInvoker pipelineInvoker, IConnection connection, string hubName, string connectionId, StateChangeTracker tracker) + { + _send = (signal, invocation, exclude) => pipelineInvoker.Send(new HubOutgoingInvokerContext(connection, signal, invocation, exclude)); + _connectionId = connectionId; + _hubName = hubName; + + Caller = new StatefulSignalProxy(_send, connectionId, PrefixHelper.HubConnectionIdPrefix, hubName, tracker); + All = AllExcept(); + Others = AllExcept(connectionId); + } + + /// + /// All connected clients. + /// + public dynamic All { get; set; } + + /// + /// All connected clients except the calling client. + /// + public dynamic Others { get; set; } + + /// + /// Represents the calling client. + /// + public dynamic Caller { get; set; } + + /// + /// Returns a dynamic representation of all clients except the calling client ones specified. + /// + /// The list of connection ids to exclude + /// A dynamic representation of all clients except the calling client ones specified. + public dynamic AllExcept(params string[] excludeConnectionIds) + { + return new ClientProxy(_send, _hubName, PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); + } + + /// + /// Returns a dynamic representation of all clients in a group except the calling client. + /// + /// The name of the group + /// A dynamic representation of all clients in a group except the calling client. + public dynamic OthersInGroup(string groupName) + { + return Group(groupName, _connectionId); + } + + /// + /// Returns a dynamic representation of the specified group. + /// + /// The name of the group + /// The list of connection ids to exclude + /// A dynamic representation of the specified group. + public dynamic Group(string groupName, params string[] excludeConnectionIds) + { + if (string.IsNullOrEmpty(groupName)) + { + throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "groupName"); + } + + return new GroupProxy(_send, groupName, _hubName, PrefixHelper.GetPrefixedConnectionIds(excludeConnectionIds)); + } + + /// + /// Returns a dynamic representation of the connection with the specified connectionid. + /// + /// The connection id + /// A dynamic representation of the specified client. + public dynamic Client(string connectionId) + { + if (string.IsNullOrEmpty(connectionId)) + { + throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); + } + + return new ConnectionIdProxy(_send, connectionId, _hubName); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs new file mode 100644 index 000000000..37a295dcf --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubContext.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubContext : IHubContext + { + public HubContext(Func, Task> send, string hubName, IConnection connection) + { + Clients = new ExternalHubConnectionContext(send, hubName); + Groups = new GroupManager(connection, PrefixHelper.GetHubGroupName(hubName)); + } + + public IHubConnectionContext Clients { get; private set; } + + public IGroupManager Groups { get; private set; } + + private class ExternalHubConnectionContext : IHubConnectionContext + { + private readonly Func, Task> _send; + private readonly string _hubName; + + public ExternalHubConnectionContext(Func, Task> send, string hubName) + { + _send = send; + _hubName = hubName; + All = AllExcept(); + } + + public dynamic All + { + get; + private set; + } + + public dynamic AllExcept(params string[] exclude) + { + return new ClientProxy(_send, _hubName, exclude); + } + + public dynamic Group(string groupName, params string[] exclude) + { + if (string.IsNullOrEmpty(groupName)) + { + throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "groupName"); + } + + return new GroupProxy(_send, groupName, _hubName, exclude); + } + + public dynamic Client(string connectionId) + { + if (string.IsNullOrEmpty(connectionId)) + { + throw new ArgumentException(Resources.Error_ArgumentNullOrEmpty, "connectionId"); + } + + return new ConnectionIdProxy(_send, connectionId, _hubName); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs new file mode 100644 index 000000000..8dc80894c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubDispatcher.cs @@ -0,0 +1,522 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Handles all communication over the hubs persistent connection. + /// + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This dispatcher makes use of many interfaces.")] + public class HubDispatcher : PersistentConnection + { + private const string HubsSuffix = "/hubs"; + + private readonly List _hubs = new List(); + private readonly bool _enableJavaScriptProxies; + private readonly bool _enableDetailedErrors; + + private IJavaScriptProxyGenerator _proxyGenerator; + private IHubManager _manager; + private IHubRequestParser _requestParser; + private IParameterResolver _binder; + private IHubPipelineInvoker _pipelineInvoker; + private IPerformanceCounterManager _counters; + private bool _isDebuggingEnabled; + + private static readonly MethodInfo _continueWithMethod = typeof(HubDispatcher).GetMethod("ContinueWith", BindingFlags.NonPublic | BindingFlags.Static); + + /// + /// Initializes an instance of the class. + /// + /// Configuration settings determining whether to enable JS proxies and provide clients with detailed hub errors. + public HubDispatcher(HubConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + _enableJavaScriptProxies = configuration.EnableJavaScriptProxies; + _enableDetailedErrors = configuration.EnableDetailedErrors; + } + + protected override TraceSource Trace + { + get + { + return TraceManager["SignalR.HubDispatcher"]; + } + } + + internal override string GroupPrefix + { + get + { + return PrefixHelper.HubGroupPrefix; + } + } + + public override void Initialize(IDependencyResolver resolver, HostContext context) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + if (context == null) + { + throw new ArgumentNullException("context"); + } + + _proxyGenerator = _enableJavaScriptProxies ? resolver.Resolve() + : new EmptyJavaScriptProxyGenerator(); + + _manager = resolver.Resolve(); + _binder = resolver.Resolve(); + _requestParser = resolver.Resolve(); + _pipelineInvoker = resolver.Resolve(); + _counters = resolver.Resolve(); + + base.Initialize(resolver, context); + } + + protected override bool AuthorizeRequest(IRequest request) + { + // Populate _hubs + string data = request.QueryStringOrForm("connectionData"); + + if (!String.IsNullOrEmpty(data)) + { + var clientHubInfo = JsonSerializer.Parse>(data); + + // If there's any hubs then perform the auth check + if (clientHubInfo != null && clientHubInfo.Any()) + { + var hubCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var hubInfo in clientHubInfo) + { + if (hubCache.ContainsKey(hubInfo.Name)) + { + throw new InvalidOperationException(Resources.Error_DuplicateHubs); + } + + // Try to find the associated hub type + HubDescriptor hubDescriptor = _manager.EnsureHub(hubInfo.Name, + _counters.ErrorsHubResolutionTotal, + _counters.ErrorsHubResolutionPerSec, + _counters.ErrorsAllTotal, + _counters.ErrorsAllPerSec); + + if (_pipelineInvoker.AuthorizeConnect(hubDescriptor, request)) + { + // Add this to the list of hub descriptors this connection is interested in + hubCache.Add(hubDescriptor.Name, hubDescriptor); + } + } + + _hubs.AddRange(hubCache.Values); + + // If we have any hubs in the list then we're authorized + return _hubs.Count > 0; + } + } + + return base.AuthorizeRequest(request); + } + + /// + /// Processes the hub's incoming method calls. + /// + protected override Task OnReceived(IRequest request, string connectionId, string data) + { + HubRequest hubRequest = _requestParser.Parse(data); + + // Create the hub + HubDescriptor descriptor = _manager.EnsureHub(hubRequest.Hub, + _counters.ErrorsHubInvocationTotal, + _counters.ErrorsHubInvocationPerSec, + _counters.ErrorsAllTotal, + _counters.ErrorsAllPerSec); + + IJsonValue[] parameterValues = hubRequest.ParameterValues; + + // Resolve the method + MethodDescriptor methodDescriptor = _manager.GetHubMethod(descriptor.Name, hubRequest.Method, parameterValues); + + if (methodDescriptor == null) + { + _counters.ErrorsHubInvocationTotal.Increment(); + _counters.ErrorsHubInvocationPerSec.Increment(); + + // Empty (noop) method descriptor + // Use: Forces the hub pipeline module to throw an error. This error is encapsulated in the HubDispatcher. + // Encapsulating it in the HubDispatcher prevents the error from bubbling up to the transport level. + // Specifically this allows us to return a faulted task (call .fail on client) and to not cause the + // transport to unintentionally fail. + methodDescriptor = new NullMethodDescriptor(hubRequest.Method); + } + + // Resolving the actual state object + var tracker = new StateChangeTracker(hubRequest.State); + var hub = CreateHub(request, descriptor, connectionId, tracker, throwIfFailedToCreate: true); + + return InvokeHubPipeline(hub, parameterValues, methodDescriptor, hubRequest, tracker) + .ContinueWith(task => hub.Dispose(), TaskContinuationOptions.ExecuteSynchronously); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flown to the caller.")] + private Task InvokeHubPipeline(IHub hub, + IJsonValue[] parameterValues, + MethodDescriptor methodDescriptor, + HubRequest hubRequest, + StateChangeTracker tracker) + { + Task piplineInvocation; + + try + { + var args = _binder.ResolveMethodParameters(methodDescriptor, parameterValues); + var context = new HubInvokerContext(hub, tracker, methodDescriptor, args); + + // Invoke the pipeline and save the task + piplineInvocation = _pipelineInvoker.Invoke(context); + } + catch (Exception ex) + { + piplineInvocation = TaskAsyncHelper.FromError(ex); + } + + // Determine if we have a faulted task or not and handle it appropriately. + return piplineInvocation.ContinueWith(task => + { + if (task.IsFaulted) + { + return ProcessResponse(tracker, result: null, request: hubRequest, error: task.Exception); + } + else if (task.IsCanceled) + { + return ProcessResponse(tracker, result: null, request: hubRequest, error: new OperationCanceledException()); + } + else + { + return ProcessResponse(tracker, task.Result, hubRequest, error: null); + } + }) + .FastUnwrap(); + } + + public override Task ProcessRequest(HostContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + // Trim any trailing slashes + string normalized = context.Request.Url.LocalPath.TrimEnd('/'); + + if (normalized.EndsWith(HubsSuffix, StringComparison.OrdinalIgnoreCase)) + { + // Generate the proper hub url + string hubUrl = normalized.Substring(0, normalized.Length - HubsSuffix.Length); + + // Generate the proxy + context.Response.ContentType = JsonUtility.JavaScriptMimeType; + return context.Response.End(_proxyGenerator.GenerateProxy(hubUrl)); + } + + _isDebuggingEnabled = context.IsDebuggingEnabled(); + + return base.ProcessRequest(context); + } + + internal static Task Connect(IHub hub) + { + return hub.OnConnected(); + } + + internal static Task Reconnect(IHub hub) + { + return hub.OnReconnected(); + } + + internal static Task Disconnect(IHub hub) + { + return hub.OnDisconnected(); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "A faulted task is returned.")] + internal static Task Incoming(IHubIncomingInvokerContext context) + { + var tcs = new TaskCompletionSource(); + + try + { + object result = context.MethodDescriptor.Invoker(context.Hub, context.Args.ToArray()); + Type returnType = context.MethodDescriptor.ReturnType; + + if (typeof(Task).IsAssignableFrom(returnType)) + { + var task = (Task)result; + if (!returnType.IsGenericType) + { + task.ContinueWith(tcs); + } + else + { + // Get the in Task + Type resultType = returnType.GetGenericArguments().Single(); + + Type genericTaskType = typeof(Task<>).MakeGenericType(resultType); + + // Get the correct ContinueWith overload + var parameter = Expression.Parameter(typeof(object)); + + // TODO: Cache this whole thing + // Action callback = result => ContinueWith((Task)result, tcs); + MethodInfo continueWithMethod = _continueWithMethod.MakeGenericMethod(resultType); + + Expression body = Expression.Call(continueWithMethod, + Expression.Convert(parameter, genericTaskType), + Expression.Constant(tcs)); + + var continueWithInvoker = Expression.Lambda>(body, parameter).Compile(); + continueWithInvoker.Invoke(result); + } + } + else + { + tcs.TrySetResult(result); + } + } + catch (Exception ex) + { + tcs.TrySetUnwrappedException(ex); + } + + return tcs.Task; + } + + internal static Task Outgoing(IHubOutgoingInvokerContext context) + { + var message = new ConnectionMessage(context.Signal, context.Invocation, context.ExcludedSignals); + + return context.Connection.Send(message); + } + + protected override Task OnConnected(IRequest request, string connectionId) + { + return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Connect(hub)); + } + + protected override Task OnReconnected(IRequest request, string connectionId) + { + return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Reconnect(hub)); + } + + protected override IList OnRejoiningGroups(IRequest request, IList groups, string connectionId) + { + return _hubs.Select(hubDescriptor => + { + string groupPrefix = hubDescriptor.Name + "."; + + var hubGroups = groups.Where(g => g.StartsWith(groupPrefix, StringComparison.OrdinalIgnoreCase)) + .Select(g => g.Substring(groupPrefix.Length)) + .ToList(); + + return _pipelineInvoker.RejoiningGroups(hubDescriptor, request, hubGroups) + .Select(g => groupPrefix + g); + + }).SelectMany(groupsToRejoin => groupsToRejoin).ToList(); + } + + protected override Task OnDisconnected(IRequest request, string connectionId) + { + return ExecuteHubEvent(request, connectionId, hub => _pipelineInvoker.Disconnect(hub)); + } + + protected override IList GetSignals(string connectionId) + { + return _hubs.SelectMany(info => new[] { PrefixHelper.GetHubName(info.Name), PrefixHelper.GetHubConnectionId(info.CreateQualifiedName(connectionId)) }) + .Concat(new[] { PrefixHelper.GetConnectionId(connectionId), PrefixHelper.GetAck(connectionId) }) + .ToList(); + } + + private Task ExecuteHubEvent(IRequest request, string connectionId, Func action) + { + var hubs = GetHubs(request, connectionId).ToList(); + var operations = hubs.Select(instance => action(instance).Catch().OrEmpty()).ToArray(); + + if (operations.Length == 0) + { + DisposeHubs(hubs); + return TaskAsyncHelper.Empty; + } + + var tcs = new TaskCompletionSource(); + Task.Factory.ContinueWhenAll(operations, tasks => + { + DisposeHubs(hubs); + var faulted = tasks.FirstOrDefault(t => t.IsFaulted); + if (faulted != null) + { + tcs.SetUnwrappedException(faulted.Exception); + } + else if (tasks.Any(t => t.IsCanceled)) + { + tcs.SetCanceled(); + } + else + { + tcs.SetResult(null); + } + }); + + return tcs.Task; + } + + private IHub CreateHub(IRequest request, HubDescriptor descriptor, string connectionId, StateChangeTracker tracker = null, bool throwIfFailedToCreate = false) + { + try + { + var hub = _manager.ResolveHub(descriptor.Name); + + if (hub != null) + { + tracker = tracker ?? new StateChangeTracker(); + + hub.Context = new HubCallerContext(request, connectionId); + hub.Clients = new HubConnectionContext(_pipelineInvoker, Connection, descriptor.Name, connectionId, tracker); + hub.Groups = new GroupManager(Connection, PrefixHelper.GetHubGroupName(descriptor.Name)); + } + + return hub; + } + catch (Exception ex) + { + Trace.TraceInformation(String.Format(CultureInfo.CurrentCulture, Resources.Error_ErrorCreatingHub + ex.Message, descriptor.Name)); + + if (throwIfFailedToCreate) + { + throw; + } + + return null; + } + } + + private IEnumerable GetHubs(IRequest request, string connectionId) + { + return from descriptor in _hubs + select CreateHub(request, descriptor, connectionId) into hub + where hub != null + select hub; + } + + private static void DisposeHubs(IEnumerable hubs) + { + foreach (var hub in hubs) + { + hub.Dispose(); + } + } + + private Task ProcessResponse(StateChangeTracker tracker, object result, HubRequest request, Exception error) + { + var hubResult = new HubResponse + { + State = tracker.GetChanges(), + Result = result, + Id = request.Id, + }; + + if (error != null) + { + _counters.ErrorsHubInvocationTotal.Increment(); + _counters.ErrorsHubInvocationPerSec.Increment(); + _counters.ErrorsAllTotal.Increment(); + _counters.ErrorsAllPerSec.Increment(); + + if (_enableDetailedErrors) + { + var exception = error.InnerException ?? error; + hubResult.StackTrace = _isDebuggingEnabled ? exception.StackTrace : null; + hubResult.Error = exception.Message; + } + else + { + hubResult.Error = String.Format(CultureInfo.CurrentCulture, Resources.Error_HubInvocationFailed, request.Hub, request.Method); + } + } + + return Transport.Send(hubResult); + } + + private static void ContinueWith(Task task, TaskCompletionSource tcs) + { + if (task.IsCompleted) + { + // Fast path for tasks that completed synchronously + ContinueSync(task, tcs); + } + else + { + ContinueAsync(task, tcs); + } + } + + private static void ContinueSync(Task task, TaskCompletionSource tcs) + { + if (task.IsFaulted) + { + tcs.TrySetUnwrappedException(task.Exception); + } + else if (task.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(task.Result); + } + } + + private static void ContinueAsync(Task task, TaskCompletionSource tcs) + { + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(t.Result); + } + }); + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "It is instantiated through JSON deserialization.")] + private class ClientHubInfo + { + public string Name { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs new file mode 100644 index 000000000..49166ce07 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubMethodNameAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public sealed class HubMethodNameAttribute : Attribute + { + public HubMethodNameAttribute(string methodName) + { + if (String.IsNullOrEmpty(methodName)) + { + throw new ArgumentNullException("methodName"); + } + MethodName = methodName; + } + + public string MethodName + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs new file mode 100644 index 000000000..942c88706 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubNameAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class HubNameAttribute : Attribute + { + public HubNameAttribute(string hubName) + { + if (String.IsNullOrEmpty(hubName)) + { + throw new ArgumentNullException("hubName"); + } + HubName = hubName; + } + + public string HubName + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs new file mode 100644 index 000000000..083d855cb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class HubRequest + { + public string Hub { get; set; } + public string Method { get; set; } + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This type is used for de-serialization.")] + public IJsonValue[] ParameterValues { get; set; } + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is used for de-serialization.")] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This type is used for de-serialization.")] + public IDictionary State { get; set; } + public string Id { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs new file mode 100644 index 000000000..f5f361f41 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubRequestParser.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubRequestParser : IHubRequestParser + { + private static readonly IJsonValue[] _emptyArgs = new IJsonValue[0]; + + public HubRequest Parse(string data) + { + var serializer = new JsonNetSerializer(); + var deserializedData = serializer.Parse(data); + + var request = new HubRequest(); + + request.Hub = deserializedData.Hub; + request.Method = deserializedData.Method; + request.Id = deserializedData.Id; + request.State = GetState(deserializedData); + request.ParameterValues = (deserializedData.Args != null) ? deserializedData.Args.Select(value => new JRawValue(value)).ToArray() : _emptyArgs; + + return request; + } + + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "This type is used for deserialzation")] + private class HubInvocation + { + [JsonProperty("H")] + public string Hub { get; set; } + [JsonProperty("M")] + public string Method { get; set; } + [JsonProperty("I")] + public string Id { get; set; } + [JsonProperty("S")] + public JRaw State { get; set; } + [JsonProperty("A")] + public JRaw[] Args { get; set; } + } + + private static IDictionary GetState(HubInvocation deserializedData) + { + if (deserializedData.State == null) + { + return new Dictionary(); + } + + // Get the raw JSON string and check if it's over 4K + string json = deserializedData.State.ToString(); + + if (json.Length > 4096) + { + throw new InvalidOperationException(Resources.Error_StateExceededMaximumLength); + } + + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new SipHashBasedDictionaryConverter()); + var serializer = new JsonNetSerializer(settings); + return serializer.Parse>(json); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs new file mode 100644 index 000000000..a1305eb9a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/HubResponse.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// The response returned from an incoming hub request. + /// + public class HubResponse + { + /// + /// The changes made the the round tripped state. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Type is used for serialization")] + [JsonProperty("S", NullValueHandling = NullValueHandling.Ignore)] + public IDictionary State { get; set; } + + /// + /// The result of the invocation. + /// + [JsonProperty("R", NullValueHandling = NullValueHandling.Ignore)] + public object Result { get; set; } + + /// + /// The id of the operation. + /// + [JsonProperty("I")] + public string Id { get; set; } + + /// + /// The exception that occurs as a result of invoking the hub method. + /// + [JsonProperty("E", NullValueHandling = NullValueHandling.Ignore)] + public string Error { get; set; } + + /// + /// The stack trace of the exception that occurs as a result of invoking the hub method. + /// + [JsonProperty("T", NullValueHandling = NullValueHandling.Ignore)] + public string StackTrace { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs new file mode 100644 index 000000000..5e98096fb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IAssemblyLocator.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public interface IAssemblyLocator + { + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Might be expensive.")] + IList GetAssemblies(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs new file mode 100644 index 000000000..71c9bc69c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IClientProxy.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A server side proxy for the client side hub. + /// + public interface IClientProxy + { + /// + /// Invokes a method on the connection(s) represented by the instance. + /// + /// name of the method to invoke + /// argumetns to pass to the client + /// A task that represents when the data has been sent to the client. + Task Invoke(string method, params object[] args); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs new file mode 100644 index 000000000..8e32d5366 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHub.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public interface IHub : IDisposable + { + /// + /// Gets a . Which contains information about the calling client. + /// + HubCallerContext Context { get; set; } + + /// + /// Gets a dynamic object that represents all clients connected to this hub (not hub instance). + /// + HubConnectionContext Clients { get; set; } + + /// + /// Gets the the hub instance. + /// + IGroupManager Groups { get; set; } + + /// + /// Called when a new connection is made to the . + /// + Task OnConnected(); + + /// + /// Called when a connection reconnects to the after a timeout. + /// + Task OnReconnected(); + + /// + /// Called when a connection is disconnected from the . + /// + Task OnDisconnected(); + } +} + diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs new file mode 100644 index 000000000..9a606a8a5 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubActivator.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public interface IHubActivator + { + IHub Create(HubDescriptor descriptor); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs new file mode 100644 index 000000000..8dae8e28c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubConnectionContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Encapsulates all information about a SignalR connection for an . + /// + public interface IHubConnectionContext + { + dynamic All { get; } + dynamic AllExcept(params string[] excludeConnectionIds); + dynamic Client(string connectionId); + dynamic Group(string groupName, params string[] excludeConnectionIds); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs new file mode 100644 index 000000000..dd798dd5f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IHubRequestParser.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Handles parsing incoming requests through the . + /// + public interface IHubRequestParser + { + /// + /// Parses the incoming hub payload into a . + /// + /// The raw hub payload. + /// The resulting . + HubRequest Parse(string data); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs new file mode 100644 index 000000000..05690b5d7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptMinifier.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public interface IJavaScriptMinifier + { + string Minify(string source); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs new file mode 100644 index 000000000..49abbe3ea --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/IJavaScriptProxyGenerator.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public interface IJavaScriptProxyGenerator + { + string GenerateProxy(string serviceUrl); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs new file mode 100644 index 000000000..b9eb9ee08 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultHubManager.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DefaultHubManager : IHubManager + { + private readonly IEnumerable _methodProviders; + private readonly IHubActivator _activator; + private readonly IEnumerable _hubProviders; + + public DefaultHubManager(IDependencyResolver resolver) + { + _hubProviders = resolver.ResolveAll(); + _methodProviders = resolver.ResolveAll(); + _activator = resolver.Resolve(); + } + + public HubDescriptor GetHub(string hubName) + { + HubDescriptor descriptor = null; + if (_hubProviders.FirstOrDefault(p => p.TryGetHub(hubName, out descriptor)) != null) + { + return descriptor; + } + + return null; + } + + public IEnumerable GetHubs(Func predicate) + { + var hubs = _hubProviders.SelectMany(p => p.GetHubs()); + + if (predicate != null) + { + return hubs.Where(predicate); + } + + return hubs; + } + + public MethodDescriptor GetHubMethod(string hubName, string method, IList parameters) + { + HubDescriptor hub = GetHub(hubName); + + if (hub == null) + { + return null; + } + + MethodDescriptor descriptor = null; + if (_methodProviders.FirstOrDefault(p => p.TryGetMethod(hub, method, out descriptor, parameters)) != null) + { + return descriptor; + } + + return null; + } + + public IEnumerable GetHubMethods(string hubName, Func predicate) + { + HubDescriptor hub = GetHub(hubName); + + if (hub == null) + { + return null; + } + + var methods = _methodProviders.SelectMany(p => p.GetMethods(hub)); + + if (predicate != null) + { + return methods.Where(predicate); + } + + return methods; + + } + + public IHub ResolveHub(string hubName) + { + HubDescriptor hub = GetHub(hubName); + return hub == null ? null : _activator.Create(hub); + } + + public IEnumerable ResolveHubs() + { + return GetHubs(predicate: null).Select(hub => _activator.Create(hub)); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs new file mode 100644 index 000000000..5b31a57f9 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/DefaultParameterResolver.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.SignalR.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class DefaultParameterResolver : IParameterResolver + { + /// + /// Resolves a parameter value based on the provided object. + /// + /// Parameter descriptor. + /// Value to resolve the parameter value from. + /// The parameter value. + public virtual object ResolveParameter(ParameterDescriptor descriptor, IJsonValue value) + { + if (descriptor == null) + { + throw new ArgumentNullException("descriptor"); + } + + if (value == null) + { + throw new ArgumentNullException("value"); + } + + if (value.GetType() == descriptor.ParameterType) + { + return value; + } + + return value.ConvertTo(descriptor.ParameterType); + } + + /// + /// Resolves method parameter values based on provided objects. + /// + /// Method descriptor. + /// List of values to resolve parameter values from. + /// Array of parameter values. + public virtual IList ResolveMethodParameters(MethodDescriptor method, IList values) + { + if (method == null) + { + throw new ArgumentNullException("method"); + } + + return method.Parameters.Zip(values, ResolveParameter).ToArray(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs new file mode 100644 index 000000000..750a70965 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/Descriptor.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public abstract class Descriptor + { + /// + /// Name of Descriptor. + /// + public virtual string Name { get; set; } + + /// + /// Flags whether the name was specified. + /// + public virtual bool NameSpecified { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs new file mode 100644 index 000000000..c4271eaef --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/HubDescriptor.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Holds information about a single hub. + /// + public class HubDescriptor : Descriptor + { + /// + /// Hub type. + /// + public virtual Type HubType { get; set; } + + public string CreateQualifiedName(string unqualifiedName) + { + return Name + "." + unqualifiedName; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs new file mode 100644 index 000000000..971f54f01 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/MethodDescriptor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Holds information about a single hub method. + /// + public class MethodDescriptor : Descriptor + { + /// + /// The return type of this method. + /// + public virtual Type ReturnType { get; set; } + + /// + /// Hub descriptor object, target to this method. + /// + public virtual HubDescriptor Hub { get; set; } + + /// + /// Available method parameters. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is supposed to be mutable")] + public virtual IList Parameters { get; set; } + + /// + /// Method invocation delegate. + /// Takes a target hub and an array of invocation arguments as it's arguments. + /// + public virtual Func Invoker { get; set; } + + /// + /// Attributes attached to this method. + /// + public virtual IEnumerable Attributes { get; set; } + } +} + diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs new file mode 100644 index 000000000..d1617dbfb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/NullMethodDescriptor.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class NullMethodDescriptor : MethodDescriptor + { + private static readonly IEnumerable _attributes = new List(); + private static readonly IList _parameters = new List(); + + private string _methodName; + + public NullMethodDescriptor(string methodName) + { + _methodName = methodName; + } + + public override Func Invoker + { + get + { + return (emptyHub, emptyParameters) => + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MethodCouldNotBeResolved, _methodName)); + }; + } + } + + public override IList Parameters + { + get { return _parameters; } + } + + public override IEnumerable Attributes + { + get { return _attributes; } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs new file mode 100644 index 000000000..4e32d3a8b --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/Descriptors/ParameterDescriptor.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Holds information about a single hub method parameter. + /// + public class ParameterDescriptor + { + /// + /// Parameter name. + /// + public virtual string Name { get; set; } + + /// + /// Parameter type. + /// + public virtual Type ParameterType { get; set; } + } +} + diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs new file mode 100644 index 000000000..c52b5e760 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/HubMethodDispatcher.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubMethodDispatcher + { + private HubMethodExecutor _executor; + + public HubMethodDispatcher(MethodInfo methodInfo) + { + _executor = GetExecutor(methodInfo); + MethodInfo = methodInfo; + } + + private delegate object HubMethodExecutor(IHub hub, object[] parameters); + + private delegate void VoidHubMethodExecutor(IHub hub, object[] parameters); + + public MethodInfo MethodInfo { get; private set; } + + public object Execute(IHub hub, object[] parameters) + { + return _executor(hub, parameters); + } + + private static HubMethodExecutor GetExecutor(MethodInfo methodInfo) + { + // Parameters to executor + ParameterExpression hubParameter = Expression.Parameter(typeof(IHub), "hub"); + ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + List parameters = new List(); + ParameterInfo[] paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + ParameterInfo paramInfo = paramInfos[i]; + BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(hubParameter, methodInfo.ReflectedType) : null; + MethodCallExpression methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // methodCall is "((TController) hub) method((T0) parameters[0], (T1) parameters[1], ...)" + // Create function + if (methodCall.Type == typeof(void)) + { + Expression lambda = Expression.Lambda(methodCall, hubParameter, parametersParameter); + VoidHubMethodExecutor voidExecutor = lambda.Compile(); + return WrapVoidAction(voidExecutor); + } + else + { + // must coerce methodCall to match HubMethodExecutor signature + UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object)); + Expression lambda = Expression.Lambda(castMethodCall, hubParameter, parametersParameter); + return lambda.Compile(); + } + } + + private static HubMethodExecutor WrapVoidAction(VoidHubMethodExecutor executor) + { + return delegate(IHub hub, object[] parameters) + { + executor(hub, parameters); + return null; + }; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs new file mode 100644 index 000000000..e439d0b92 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubDescriptorProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Describes hub descriptor provider, which provides information about available hubs. + /// + public interface IHubDescriptorProvider + { + /// + /// Retrieve all avaiable hubs. + /// + /// Collection of hub descriptors. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This call might be expensive")] + IList GetHubs(); + + /// + /// Tries to retrieve hub with a given name. + /// + /// Name of the hub. + /// Retrieved descriptor object. + /// True, if hub has been found + bool TryGetHub(string hubName, out HubDescriptor descriptor); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs new file mode 100644 index 000000000..53868eefe --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IHubManager.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Describes a hub manager - main point in the whole hub and method lookup process. + /// + public interface IHubManager + { + /// + /// Retrieves a single hub descriptor. + /// + /// Name of the hub. + /// Hub descriptor, if found. Null, otherwise. + HubDescriptor GetHub(string hubName); + + /// + /// Retrieves all available hubs matching the given predicate. + /// + /// List of hub descriptors. + IEnumerable GetHubs(Func predicate); + + /// + /// Resolves a given hub name to a concrete object. + /// + /// Name of the hub. + /// Hub implementation instance, if found. Null otherwise. + IHub ResolveHub(string hubName); + + /// + /// Resolves all available hubs to their concrete objects. + /// + /// List of hub instances. + IEnumerable ResolveHubs(); + + /// + /// Retrieves a method with a given name on a given hub. + /// + /// Name of the hub. + /// Name of the method to find. + /// Method parameters to match. + /// Descriptor of the method, if found. Null otherwise. + MethodDescriptor GetHubMethod(string hubName, string method, IList parameters); + + /// + /// Gets all methods available to call on a given hub. + /// + /// Name of the hub, + /// Optional predicate for filtering results. + /// List of available methods. + IEnumerable GetHubMethods(string hubName, Func predicate); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs new file mode 100644 index 000000000..3f88b537f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IMethodDescriptorProvider.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Describes a hub method provider that builds a collection of available methods on a given hub. + /// + public interface IMethodDescriptorProvider + { + /// + /// Retrieve all methods on a given hub. + /// + /// Hub descriptor object. + /// Available methods. + IEnumerable GetMethods(HubDescriptor hub); + + /// + /// Tries to retrieve a method. + /// + /// Hub descriptor object + /// Name of the method. + /// Descriptor of the method, if found. Null otherwise. + /// Method parameters to match. + /// True, if a method has been found. + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "This is a well known pattern for efficient lookup")] + bool TryGetMethod(HubDescriptor hub, string method, out MethodDescriptor descriptor, IList parameters); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs new file mode 100644 index 000000000..c91bac1c2 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/IParameterResolver.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.SignalR.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Describes a parameter resolver for resolving parameter-matching values based on provided information. + /// + public interface IParameterResolver + { + /// + /// Resolves method parameter values based on provided objects. + /// + /// Method descriptor. + /// List of values to resolve parameter values from. + /// Array of parameter values. + IList ResolveMethodParameters(MethodDescriptor method, IList values); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs new file mode 100644 index 000000000..49b1e1d44 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedHubDescriptorProvider.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class ReflectedHubDescriptorProvider : IHubDescriptorProvider + { + private readonly Lazy> _hubs; + private readonly Lazy _locator; + + public ReflectedHubDescriptorProvider(IDependencyResolver resolver) + { + _locator = new Lazy(resolver.Resolve); + _hubs = new Lazy>(BuildHubsCache); + } + + public IList GetHubs() + { + return _hubs.Value + .Select(kv => kv.Value) + .Distinct() + .ToList(); + } + + public bool TryGetHub(string hubName, out HubDescriptor descriptor) + { + return _hubs.Value.TryGetValue(hubName, out descriptor); + } + + protected IDictionary BuildHubsCache() + { + // Getting all IHub-implementing types that apply + var types = _locator.Value.GetAssemblies() + .SelectMany(GetTypesSafe) + .Where(IsHubType); + + // Building cache entries for each descriptor + // Each descriptor is stored in dictionary under a key + // that is it's name or the name provided by an attribute + var cacheEntries = types + .Select(type => new HubDescriptor + { + NameSpecified = (type.GetHubAttributeName() != null), + Name = type.GetHubName(), + HubType = type + }) + .ToDictionary(hub => hub.Name, + hub => hub, + StringComparer.OrdinalIgnoreCase); + + return cacheEntries; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "If we throw then it's not a hub type")] + private static bool IsHubType(Type type) + { + try + { + return typeof(IHub).IsAssignableFrom(type) && + !type.IsAbstract && + (type.Attributes.HasFlag(TypeAttributes.Public) || + type.Attributes.HasFlag(TypeAttributes.NestedPublic)); + } + catch + { + return false; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "If we throw then we have an empty type")] + private static IEnumerable GetTypesSafe(Assembly a) + { + try + { + return a.GetTypes(); + } + catch + { + return Enumerable.Empty(); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs new file mode 100644 index 000000000..db6b95756 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Lookup/ReflectedMethodDescriptorProvider.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.SignalR.Json; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class ReflectedMethodDescriptorProvider : IMethodDescriptorProvider + { + private readonly ConcurrentDictionary>> _methods; + private readonly ConcurrentDictionary _executableMethods; + + public ReflectedMethodDescriptorProvider() + { + _methods = new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); + _executableMethods = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable GetMethods(HubDescriptor hub) + { + return FetchMethodsFor(hub) + .SelectMany(kv => kv.Value) + .ToList(); + } + + /// + /// Retrieves an existing dictionary of all available methods for a given hub from cache. + /// If cache entry does not exist - it is created automatically by BuildMethodCacheFor. + /// + /// + /// + private IDictionary> FetchMethodsFor(HubDescriptor hub) + { + return _methods.GetOrAdd( + hub.Name, + key => BuildMethodCacheFor(hub)); + } + + /// + /// Builds a dictionary of all possible methods on a given hub. + /// Single entry contains a collection of available overloads for a given method name (key). + /// This dictionary is being cached afterwards. + /// + /// Hub to build cache for + /// Dictionary of available methods + private static IDictionary> BuildMethodCacheFor(HubDescriptor hub) + { + return ReflectionHelper.GetExportedHubMethods(hub.HubType) + .GroupBy(GetMethodName, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, + group => group.Select(oload => + new MethodDescriptor + { + ReturnType = oload.ReturnType, + Name = group.Key, + NameSpecified = (GetMethodAttributeName(oload) != null), + Invoker = new HubMethodDispatcher(oload).Execute, + Hub = hub, + Attributes = oload.GetCustomAttributes(typeof(Attribute), inherit: true).Cast(), + Parameters = oload.GetParameters() + .Select(p => new ParameterDescriptor + { + Name = p.Name, + ParameterType = p.ParameterType, + }) + .ToList() + }), + StringComparer.OrdinalIgnoreCase); + } + + /// + /// Searches the specified Hub for the specified . + /// + /// + /// In the case that there are multiple overloads of the specified , the parameter set helps determine exactly which instance of the overload should be resolved. + /// If there are multiple overloads found with the same number of matching parameters, none of the methods will be returned because it is not possible to determine which overload of the method was intended to be resolved. + /// + /// Hub to search for the specified on. + /// The method name to search for. + /// If successful, the that was resolved. + /// The set of parameters that will be used to help locate a specific overload of the specified . + /// True if the method matching the name/parameter set is found on the hub, otherwise false. + public bool TryGetMethod(HubDescriptor hub, string method, out MethodDescriptor descriptor, IList parameters) + { + string hubMethodKey = BuildHubExecutableMethodCacheKey(hub, method, parameters); + + if (!_executableMethods.TryGetValue(hubMethodKey, out descriptor)) + { + IEnumerable overloads; + + if (FetchMethodsFor(hub).TryGetValue(method, out overloads)) + { + var matches = overloads.Where(o => o.Matches(parameters)).ToList(); + + // If only one match is found, that is the "executable" version, otherwise none of the methods can be returned because we don't know which one was actually being targeted + descriptor = matches.Count == 1 ? matches[0] : null; + } + else + { + descriptor = null; + } + + // If an executable method was found, cache it for future lookups (NOTE: we don't cache null instances because it could be a surface area for DoS attack by supplying random method names to flood the cache) + if (descriptor != null) + { + _executableMethods.TryAdd(hubMethodKey, descriptor); + } + } + + return descriptor != null; + } + + private static string BuildHubExecutableMethodCacheKey(HubDescriptor hub, string method, IList parameters) + { + string normalizedParameterCountKeyPart; + + if (parameters != null) + { + normalizedParameterCountKeyPart = parameters.Count.ToString(CultureInfo.InvariantCulture); + } + else + { + // NOTE: we normalize a null parameter array to be the same as an empty (i.e. Length == 0) parameter array + normalizedParameterCountKeyPart = "0"; + } + + // NOTE: we always normalize to all uppercase since method names are case insensitive and could theoretically come in diff. variations per call + string normalizedMethodName = method.ToUpperInvariant(); + + string methodKey = hub.Name + "::" + normalizedMethodName + "(" + normalizedParameterCountKeyPart + ")"; + + return methodKey; + } + + private static string GetMethodName(MethodInfo method) + { + return GetMethodAttributeName(method) ?? method.Name; + } + + private static string GetMethodAttributeName(MethodInfo method) + { + return ReflectionHelper.GetAttributeValue(method, a => a.MethodName); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs new file mode 100644 index 000000000..4d944017e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullClientProxy.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Dynamic; +using System.Globalization; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class NullClientProxy : DynamicObject + { + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UsingHubInstanceNotCreatedUnsupported)); + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UsingHubInstanceNotCreatedUnsupported)); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs new file mode 100644 index 000000000..8eca68d91 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/NullJavaScriptMinifier.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class NullJavaScriptMinifier : IJavaScriptMinifier + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This is a singleton")] + public static readonly NullJavaScriptMinifier Instance = new NullJavaScriptMinifier(); + + public string Minify(string source) + { + return source; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs new file mode 100644 index 000000000..046e45cba --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/AuthorizeModule.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// This module is added the the HubPipeline by default. + /// + /// Hub level attributes that implement such as are applied to determine + /// whether to allow potential clients to receive messages sent from that hub using a or a + /// All applicable hub attributes must allow hub connection for the connection to be authorized. + /// + /// Hub and method level attributes that implement such as are applied + /// to determine whether to allow callers to invoke hub methods. + /// All applicable hub level AND method level attributes must allow hub method invocation for the invocation to be authorized. + /// + /// Optionally, this module may be instantiated with and + /// authorizers that will be applied globally to all hubs and hub methods. + /// + public class AuthorizeModule : HubPipelineModule + { + // Global authorizers + private readonly IAuthorizeHubConnection _globalConnectionAuthorizer; + private readonly IAuthorizeHubMethodInvocation _globalInvocationAuthorizer; + + // Attribute authorizer caches + private readonly ConcurrentDictionary> _connectionAuthorizersCache; + private readonly ConcurrentDictionary> _classInvocationAuthorizersCache; + private readonly ConcurrentDictionary> _methodInvocationAuthorizersCache; + + // By default, this module does not include any authorizers that are applied globally. + // This module will always apply authorizers attached to hubs or hub methods + public AuthorizeModule() + : this(globalConnectionAuthorizer: null, globalInvocationAuthorizer: null) + { + } + + public AuthorizeModule(IAuthorizeHubConnection globalConnectionAuthorizer, IAuthorizeHubMethodInvocation globalInvocationAuthorizer) + { + // Set global authorizers + _globalConnectionAuthorizer = globalConnectionAuthorizer; + _globalInvocationAuthorizer = globalInvocationAuthorizer; + + // Initialize attribute authorizer caches + _connectionAuthorizersCache = new ConcurrentDictionary>(); + _classInvocationAuthorizersCache = new ConcurrentDictionary>(); + _methodInvocationAuthorizersCache = new ConcurrentDictionary>(); + } + + public override Func BuildAuthorizeConnect(Func authorizeConnect) + { + return base.BuildAuthorizeConnect((hubDescriptor, request) => + { + // Execute custom modules first and short circuit if any deny authorization. + if (!authorizeConnect(hubDescriptor, request)) + { + return false; + } + + // Execute the global hub connection authorizer if there is one next and short circuit if it denies authorization. + if (_globalConnectionAuthorizer != null && !_globalConnectionAuthorizer.AuthorizeHubConnection(hubDescriptor, request)) + { + return false; + } + + // Get hub attributes implementing IAuthorizeHubConnection from the cache + // If the attributes do not exist in the cache, retrieve them using reflection and add them to the cache + var attributeAuthorizers = _connectionAuthorizersCache.GetOrAdd(hubDescriptor.HubType, + hubType => hubType.GetCustomAttributes(typeof(IAuthorizeHubConnection), inherit: true).Cast()); + + // Every attribute (if any) implementing IAuthorizeHubConnection attached to the relevant hub MUST allow the connection + return attributeAuthorizers.All(a => a.AuthorizeHubConnection(hubDescriptor, request)); + }); + } + + public override Func> BuildIncoming(Func> invoke) + { + return base.BuildIncoming(context => + { + // Execute the global method invocation authorizer if there is one and short circuit if it denies authorization. + if (_globalInvocationAuthorizer == null || _globalInvocationAuthorizer.AuthorizeHubMethodInvocation(context, appliesToMethod: false)) + { + // Get hub attributes implementing IAuthorizeHubMethodInvocation from the cache + // If the attributes do not exist in the cache, retrieve them using reflection and add them to the cache + var classLevelAuthorizers = _classInvocationAuthorizersCache.GetOrAdd(context.Hub.GetType(), + hubType => hubType.GetCustomAttributes(typeof(IAuthorizeHubMethodInvocation), inherit: true).Cast()); + + // Execute all hub level authorizers and short circuit if ANY deny authorization. + if (classLevelAuthorizers.All(a => a.AuthorizeHubMethodInvocation(context, appliesToMethod: false))) + { + // If the MethodDescriptor is a NullMethodDescriptor, we don't want to cache it since a new one is created + // for each invocation with an invalid method name. #1801 + if (context.MethodDescriptor is NullMethodDescriptor) + { + return invoke(context); + } + + // Get method attributes implementing IAuthorizeHubMethodInvocation from the cache + // If the attributes do not exist in the cache, retrieve them from the MethodDescriptor and add them to the cache + var methodLevelAuthorizers = _methodInvocationAuthorizersCache.GetOrAdd(context.MethodDescriptor, + methodDescriptor => methodDescriptor.Attributes.OfType()); + + // Execute all method level authorizers. If ALL provide authorization, continue executing the invocation pipeline. + if (methodLevelAuthorizers.All(a => a.AuthorizeHubMethodInvocation(context, appliesToMethod: true))) + { + return invoke(context); + } + } + } + + // Send error back to the client + return TaskAsyncHelper.FromError( + new NotAuthorizedException(String.Format(CultureInfo.CurrentCulture, Resources.Error_CallerNotAuthorizedToInvokeMethodOn, + context.MethodDescriptor.Name, + context.MethodDescriptor.Hub.Name))); + }); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs new file mode 100644 index 000000000..c89e60014 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubConnection.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Interface to be implemented by s that can authorize client to connect to a . + /// + public interface IAuthorizeHubConnection + { + /// + /// Given a , determine whether client is authorized to connect to . + /// + /// Description of the hub client is attempting to connect to. + /// The connection request from the client. + /// true if the caller is authorized to connect to the hub; otherwise, false. + bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs new file mode 100644 index 000000000..4d1eee752 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/IAuthorizeHubMethodInvocation.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Interface to be implemented by s that can authorize the invocation of methods. + /// + public interface IAuthorizeHubMethodInvocation + { + /// + /// Given a , determine whether client is authorized to invoke the method. + /// + /// An providing details regarding the method invocation. + /// Indicates whether the interface instance is an attribute applied directly to a method. + /// true if the caller is authorized to invoke the method; otherwise, false. + bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs new file mode 100644 index 000000000..10d32b649 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/Auth/NotAuthorizedException.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + [Serializable] + public class NotAuthorizedException : Exception + { + public NotAuthorizedException() { } + public NotAuthorizedException(string message) : base(message) { } + public NotAuthorizedException(string message, Exception inner) : base(message, inner) { } + protected NotAuthorizedException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) + : base(info, context) { } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs new file mode 100644 index 000000000..eea40b052 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubInvokerContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubInvokerContext : IHubIncomingInvokerContext + { + public HubInvokerContext(IHub hub, StateChangeTracker tracker, MethodDescriptor methodDescriptor, IList args) + { + Hub = hub; + MethodDescriptor = methodDescriptor; + Args = args; + StateTracker = tracker; + } + + public IHub Hub + { + get; + private set; + } + + public MethodDescriptor MethodDescriptor + { + get; + private set; + } + + public IList Args + { + get; + private set; + } + + + public StateChangeTracker StateTracker + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs new file mode 100644 index 000000000..f36527935 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubOutgoingInvokerContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubOutgoingInvokerContext : IHubOutgoingInvokerContext + { + public HubOutgoingInvokerContext(IConnection connection, string signal, ClientHubInvocation invocation, IList excludedSignals) + { + Connection = connection; + Signal = signal; + Invocation = invocation; + ExcludedSignals = excludedSignals; + } + + public IConnection Connection + { + get; + private set; + } + + public ClientHubInvocation Invocation + { + get; + private set; + } + + public string Signal + { + get; + private set; + } + + public IList ExcludedSignals + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs new file mode 100644 index 000000000..9cf6eccd4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipeline.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + internal class HubPipeline : IHubPipeline, IHubPipelineInvoker + { + private readonly Stack _modules; + private readonly Lazy _pipeline; + + public HubPipeline() + { + _modules = new Stack(); + _pipeline = new Lazy(() => new ComposedPipeline(_modules)); + } + + public IHubPipeline AddModule(IHubPipelineModule pipelineModule) + { + if (_pipeline.IsValueCreated) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UnableToAddModulePiplineAlreadyInvoked)); + } + _modules.Push(pipelineModule); + return this; + } + + private ComposedPipeline Pipeline + { + get { return _pipeline.Value; } + } + + public Task Invoke(IHubIncomingInvokerContext context) + { + return Pipeline.Invoke(context); + } + + public Task Connect(IHub hub) + { + return Pipeline.Connect(hub); + } + + public Task Reconnect(IHub hub) + { + return Pipeline.Reconnect(hub); + } + + public Task Disconnect(IHub hub) + { + return Pipeline.Disconnect(hub); + } + + public bool AuthorizeConnect(HubDescriptor hubDescriptor, IRequest request) + { + return Pipeline.AuthorizeConnect(hubDescriptor, request); + } + + public IList RejoiningGroups(HubDescriptor hubDescriptor, IRequest request, IList groups) + { + return Pipeline.RejoiningGroups(hubDescriptor, request, groups); + } + + public Task Send(IHubOutgoingInvokerContext context) + { + return Pipeline.Send(context); + } + + private class ComposedPipeline + { + + public Func> Invoke; + public Func Connect; + public Func Reconnect; + public Func Disconnect; + public Func AuthorizeConnect; + public Func, IList> RejoiningGroups; + public Func Send; + + public ComposedPipeline(Stack modules) + { + // This wouldn't look nearly as gnarly if C# had better type inference, but now we don't need the ComposedModule or PassThroughModule. + Invoke = Compose>>(modules, (m, f) => m.BuildIncoming(f))(HubDispatcher.Incoming); + Connect = Compose>(modules, (m, f) => m.BuildConnect(f))(HubDispatcher.Connect); + Reconnect = Compose>(modules, (m, f) => m.BuildReconnect(f))(HubDispatcher.Reconnect); + Disconnect = Compose>(modules, (m, f) => m.BuildDisconnect(f))(HubDispatcher.Disconnect); + AuthorizeConnect = Compose>(modules, (m, f) => m.BuildAuthorizeConnect(f))((h, r) => true); + RejoiningGroups = Compose, IList>>(modules, (m, f) => m.BuildRejoiningGroups(f))((h, r, g) => g); + Send = Compose>(modules, (m, f) => m.BuildOutgoing(f))(HubDispatcher.Outgoing); + } + + // IHubPipelineModule could be turned into a second generic parameter, but it would make the above invocations even longer than they currently are. + private static Func Compose(IEnumerable modules, Func method) + { + // Notice we are reversing and aggregating in one step. (Function composition is associative) + return modules.Aggregate>(x => x, (a, b) => (x => method(b, a(x)))); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs new file mode 100644 index 000000000..0eaac77b8 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR +{ + public static class HubPipelineExtensions + { + /// + /// Requiring Authentication adds an to the with + /// and authorizers that will be applied globally to all hubs and hub methods. + /// These authorizers require that the 's + /// IsAuthenticated for any clients that invoke server-side hub methods or receive client-side hub method invocations. + /// + /// The to which the will be added. + public static void RequireAuthentication(this IHubPipeline pipeline) + { + if (pipeline == null) + { + throw new ArgumentNullException("pipeline"); + } + + var authorizer = new AuthorizeAttribute(); + pipeline.AddModule(new AuthorizeModule(globalConnectionAuthorizer: authorizer, globalInvocationAuthorizer: authorizer)); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs new file mode 100644 index 000000000..d914c34f0 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/HubPipelineModule.cs @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Common base class to simplify the implementation of IHubPipelineModules. + /// A module can intercept and customize various stages of hub processing such as connecting, reconnecting, disconnecting, + /// invoking server-side hub methods, invoking client-side hub methods, authorizing hub clients and rejoining hub groups. + /// A module can be activated by calling . + /// The combined modules added to the are invoked via the + /// interface. + /// + public abstract class HubPipelineModule : IHubPipelineModule + { + /// + /// Wraps a function that invokes a server-side hub method. Even if a client has not been authorized to connect + /// to a hub, it will still be authorized to invoke server-side methods on that hub unless it is prevented in + /// by not executing the invoke parameter. + /// + /// A function that invokes a server-side hub method. + /// A wrapped function that invokes a server-side hub method. + public virtual Func> BuildIncoming(Func> invoke) + { + return context => + { + if (OnBeforeIncoming(context)) + { + return invoke(context).OrEmpty() + .Then(result => OnAfterIncoming(result, context)) + .Catch(ex => OnIncomingError(ex, context)); + } + + return TaskAsyncHelper.FromResult(null); + }; + } + + /// + /// Wraps a function that is called when a client connects to the for each + /// the client connects to. By default, this results in the 's + /// OnConnected method being invoked. + /// + /// A function to be called when a client connects to a hub. + /// A wrapped function to be called when a client connects to a hub. + public virtual Func BuildConnect(Func connect) + { + return hub => + { + if (OnBeforeConnect(hub)) + { + return connect(hub).OrEmpty().Then(h => OnAfterConnect(h), hub); + } + + return TaskAsyncHelper.Empty; + }; + } + + /// + /// Wraps a function that is called when a client reconnects to the for each + /// the client connects to. By default, this results in the 's + /// OnReconnected method being invoked. + /// + /// A function to be called when a client reconnects to a hub. + /// A wrapped function to be called when a client reconnects to a hub. + public virtual Func BuildReconnect(Func reconnect) + { + return (hub) => + { + if (OnBeforeReconnect(hub)) + { + return reconnect(hub).OrEmpty().Then(h => OnAfterReconnect(h), hub); + } + return TaskAsyncHelper.Empty; + }; + } + + /// + /// Wraps a function that is called when a client disconnects from the for each + /// the client was connected to. By default, this results in the 's + /// OnDisconnected method being invoked. + /// + /// A function to be called when a client disconnects from a hub. + /// A wrapped function to be called when a client disconnects from a hub. + public virtual Func BuildDisconnect(Func disconnect) + { + return hub => + { + if (OnBeforeDisconnect(hub)) + { + return disconnect(hub).OrEmpty().Then(h => OnAfterDisconnect(h), hub); + } + + return TaskAsyncHelper.Empty; + }; + } + + /// + /// Wraps a function to be called before a client subscribes to signals belonging to the hub described by the + /// . By default, the will look for attributes on the + /// to help determine if the client is authorized to subscribe to method invocations for the + /// described hub. + /// The function returns true if the client is authorized to subscribe to client-side hub method + /// invocations; false, otherwise. + /// + /// + /// A function that dictates whether or not the client is authorized to connect to the described Hub. + /// + /// + /// A wrapped function that dictates whether or not the client is authorized to connect to the described Hub. + /// + public virtual Func BuildAuthorizeConnect(Func authorizeConnect) + { + return (hubDescriptor, request) => + { + if (OnBeforeAuthorizeConnect(hubDescriptor, request)) + { + return authorizeConnect(hubDescriptor, request); + } + return false; + }; + } + + /// + /// Wraps a function that determines which of the groups belonging to the hub described by the + /// the client should be allowed to rejoin. + /// By default, clients will rejoin all the groups they were in prior to reconnecting. + /// + /// A function that determines which groups the client should be allowed to rejoin. + /// A wrapped function that determines which groups the client should be allowed to rejoin. + public virtual Func, IList> BuildRejoiningGroups(Func, IList> rejoiningGroups) + { + return rejoiningGroups; + } + + /// + /// Wraps a function that invokes a client-side hub method. + /// + /// A function that invokes a client-side hub method. + /// A wrapped function that invokes a client-side hub method. + public virtual Func BuildOutgoing(Func send) + { + return context => + { + if (OnBeforeOutgoing(context)) + { + return send(context).OrEmpty().Then(ctx => OnAfterOutgoing(ctx), context); + } + + return TaskAsyncHelper.Empty; + }; + } + + /// + /// This method is called before the AuthorizeConnect components of any modules added later to the + /// are executed. If this returns false, then those later-added modules will not run and the client will not be allowed + /// to subscribe to client-side invocations of methods belonging to the hub defined by the . + /// + /// A description of the hub the client is trying to subscribe to. + /// The connect request of the client trying to subscribe to the hub. + /// true, if the client is authorized to connect to the hub, false otherwise. + protected virtual bool OnBeforeAuthorizeConnect(HubDescriptor hubDescriptor, IRequest request) + { + return true; + } + + /// + /// This method is called before the connect components of any modules added later to the are + /// executed. If this returns false, then those later-added modules and the method will + /// not be run. + /// + /// The hub the client has connected to. + /// + /// true, if the connect components of later added modules and the method should be executed; + /// false, otherwise. + /// + protected virtual bool OnBeforeConnect(IHub hub) + { + return true; + } + + /// + /// This method is called after the connect components of any modules added later to the are + /// executed and after is executed, if at all. + /// + /// The hub the client has connected to. + protected virtual void OnAfterConnect(IHub hub) + { + + } + + /// + /// This method is called before the reconnect components of any modules added later to the are + /// executed. If this returns false, then those later-added modules and the method will + /// not be run. + /// + /// The hub the client has reconnected to. + /// + /// true, if the reconnect components of later added modules and the method should be executed; + /// false, otherwise. + /// + protected virtual bool OnBeforeReconnect(IHub hub) + { + return true; + } + + /// + /// This method is called after the reconnect components of any modules added later to the are + /// executed and after is executed, if at all. + /// + /// The hub the client has reconnected to. + protected virtual void OnAfterReconnect(IHub hub) + { + + } + + /// + /// This method is called before the outgoing components of any modules added later to the are + /// executed. If this returns false, then those later-added modules and the client-side hub method invocation(s) will not + /// be executed. + /// + /// A description of the client-side hub method invocation. + /// + /// true, if the outgoing components of later added modules and the client-side hub method invocation(s) should be executed; + /// false, otherwise. + /// + protected virtual bool OnBeforeOutgoing(IHubOutgoingInvokerContext context) + { + return true; + } + + /// + /// This method is called after the outgoing components of any modules added later to the are + /// executed. This does not mean that all the clients have received the hub method invocation, but it does indicate indicate + /// a hub invocation message has successfully been published to a message bus. + /// + /// A description of the client-side hub method invocation. + protected virtual void OnAfterOutgoing(IHubOutgoingInvokerContext context) + { + + } + + /// + /// This method is called before the disconnect components of any modules added later to the are + /// executed. If this returns false, then those later-added modules and the method will + /// not be run. + /// + /// The hub the client has disconnected from. + /// + /// true, if the disconnect components of later added modules and the method should be executed; + /// false, otherwise. + /// + protected virtual bool OnBeforeDisconnect(IHub hub) + { + return true; + } + + /// + /// This method is called after the disconnect components of any modules added later to the are + /// executed and after is executed, if at all. + /// + /// The hub the client has disconnected from. + protected virtual void OnAfterDisconnect(IHub hub) + { + + } + + /// + /// This method is called before the incoming components of any modules added later to the are + /// executed. If this returns false, then those later-added modules and the server-side hub method invocation will not + /// be executed. Even if a client has not been authorized to connect to a hub, it will still be authorized to invoke + /// server-side methods on that hub unless it is prevented in by not + /// executing the invoke parameter or prevented in by returning false. + /// + /// A description of the server-side hub method invocation. + /// + /// true, if the incoming components of later added modules and the server-side hub method invocation should be executed; + /// false, otherwise. + /// + protected virtual bool OnBeforeIncoming(IHubIncomingInvokerContext context) + { + return true; + } + + /// + /// This method is called after the incoming components of any modules added later to the + /// and the server-side hub method have completed execution. + /// + /// The return value of the server-side hub method + /// A description of the server-side hub method invocation. + /// The possibly new or updated return value of the server-side hub method + protected virtual object OnAfterIncoming(object result, IHubIncomingInvokerContext context) + { + return result; + } + + /// + /// This is called when an uncaught exception is thrown by a server-side hub method or the incoming component of a + /// module added later to the . Observing the exception using this method will not prevent + /// it from bubbling up to other modules. + /// + /// The exception that was thrown during the server-side invocation. + /// A description of the server-side hub method invocation. + protected virtual void OnIncomingError(Exception ex, IHubIncomingInvokerContext context) + { + + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs new file mode 100644 index 000000000..a57d92b36 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubIncomingInvokerContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A description of a server-side hub method invocation originating from a client. + /// + public interface IHubIncomingInvokerContext + { + /// + /// A hub instance that contains the invoked method as a member. + /// + IHub Hub { get; } + + /// + /// A description of the method being invoked by the client. + /// + MethodDescriptor MethodDescriptor { get; } + + /// + /// The arguments to be passed to the invoked method. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This represents an ordered list of parameter values")] + IList Args { get; } + + /// + /// A key-value store representing the hub state on the client at the time of the invocation. + /// + StateChangeTracker StateTracker { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs new file mode 100644 index 000000000..9f69af582 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubOutgoingInvokerContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A description of a client-side hub method invocation originating from the server. + /// + public interface IHubOutgoingInvokerContext + { + /// + /// The , if any, corresponding to the client that invoked the server-side hub method + /// that is invoking the client-side hub method. + /// + IConnection Connection { get; } + + /// + /// A description of the method call to be made on the client. + /// + ClientHubInvocation Invocation { get; } + + /// + /// The signal (ConnectionId, hub type name or hub type name + "." + group name) belonging to clients that + /// receive the method invocation. + /// + string Signal { get; } + + /// + /// The signals (ConnectionId, hub type name or hub type name + "." + group name) belonging to clients that should + /// not receive the method invocation regardless of the . + /// + IList ExcludedSignals { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs new file mode 100644 index 000000000..d0f4c58eb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipeline.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A collection of modules that can intercept and customize various stages of hub processing such as connecting, + /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing + /// hub clients and rejoining hub groups. + /// + public interface IHubPipeline + { + /// + /// Adds an to the hub pipeline. Modules added to the pipeline first will wrap + /// modules that are added to the pipeline later. All modules must be added to the pipeline before any methods + /// on the are invoked. + /// + /// + /// A module that may intercept and customize various stages of hub processing such as connecting, + /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing + /// hub clients and rejoining hub groups. + /// + /// + /// The itself with the newly added module allowing + /// calls to be chained. + /// This method mutates the pipeline it is invoked on so it is not necessary to store its result. + /// + IHubPipeline AddModule(IHubPipelineModule pipelineModule); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs new file mode 100644 index 000000000..499e9c972 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineInvoker.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// Implementations of this interface are responsible for executing operation required to complete various stages + /// hub processing such as connecting, reconnecting, disconnecting, invoking server-side hub methods, invoking + /// client-side hub methods, authorizing hub clients and rejoining hub groups. + /// + public interface IHubPipelineInvoker + { + /// + /// Invokes a server-side hub method. + /// + /// A description of the server-side hub method invocation. + /// An asynchronous operation giving the return value of the server-side hub method invocation. + Task Invoke(IHubIncomingInvokerContext context); + + /// + /// Invokes a client-side hub method. + /// + /// A description of the client-side hub method invocation. + Task Send(IHubOutgoingInvokerContext context); + + /// + /// To be called when a client connects to the for each the client + /// connects to. By default, this results in the 's OnConnected method being invoked. + /// + /// A the client is connected to. + Task Connect(IHub hub); + + /// + /// To be called when a client reconnects to the for each the client + /// connects to. By default, this results in the 's OnReconnected method being invoked. + /// + /// A the client is reconnected to. + Task Reconnect(IHub hub); + + /// + /// To be called when a client disconnects from the for each the client + /// was connected to. By default, this results in the 's OnDisconnected method being invoked. + /// + /// A the client was disconnected from. + Task Disconnect(IHub hub); + + /// + /// To be called before a client subscribes to signals belonging to the hub described by the . + /// By default, the will look for attributes on the to help determine if + /// the client is authorized to subscribe to method invocations for the described hub. + /// + /// A description of the hub the client is attempting to connect to. + /// + /// The connect request being made by the client which should include the client's + /// User. + /// + /// true, if the client is authorized to subscribe to client-side hub method invocations; false, otherwise. + bool AuthorizeConnect(HubDescriptor hubDescriptor, IRequest request); + + /// + /// This method determines which of the groups belonging to the hub described by the the client should be + /// allowed to rejoin. + /// By default, clients that are reconnecting to the server will be removed from all groups they may have previously been a member of, + /// because untrusted clients may claim to be a member of groups they were never authorized to join. + /// + /// A description of the hub for which the client is attempting to rejoin groups. + /// The reconnect request being made by the client that is attempting to rejoin groups. + /// + /// The list of groups belonging to the relevant hub that the client claims to have been a member of before the reconnect. + /// + /// A list of groups the client is allowed to rejoin. + IList RejoiningGroups(HubDescriptor hubDescriptor, IRequest request, IList groups); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs new file mode 100644 index 000000000..79a01717c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/Pipeline/IHubPipelineModule.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// An can intercept and customize various stages of hub processing such as connecting, + /// reconnecting, disconnecting, invoking server-side hub methods, invoking client-side hub methods, authorizing hub + /// clients and rejoining hub groups. + /// Modules can be be activated by calling . + /// The combined modules added to the are invoked via the + /// interface. + /// + public interface IHubPipelineModule + { + /// + /// Wraps a function that invokes a server-side hub method. Even if a client has not been authorized to connect + /// to a hub, it will still be authorized to invoke server-side methods on that hub unless it is prevented in + /// by not executing the invoke parameter. + /// + /// A function that invokes a server-side hub method. + /// A wrapped function that invokes a server-side hub method. + Func> BuildIncoming(Func> invoke); + + /// + /// Wraps a function that invokes a client-side hub method. + /// + /// A function that invokes a client-side hub method. + /// A wrapped function that invokes a client-side hub method. + Func BuildOutgoing(Func send); + + /// + /// Wraps a function that is called when a client connects to the for each + /// the client connects to. By default, this results in the 's + /// OnConnected method being invoked. + /// + /// A function to be called when a client connects to a hub. + /// A wrapped function to be called when a client connects to a hub. + Func BuildConnect(Func connect); + + /// + /// Wraps a function that is called when a client reconnects to the for each + /// the client connects to. By default, this results in the 's + /// OnReconnected method being invoked. + /// + /// A function to be called when a client reconnects to a hub. + /// A wrapped function to be called when a client reconnects to a hub. + Func BuildReconnect(Func reconnect); + + /// + /// Wraps a function that is called when a client disconnects from the for each + /// the client was connected to. By default, this results in the 's + /// OnDisconnected method being invoked. + /// + /// A function to be called when a client disconnects from a hub. + /// A wrapped function to be called when a client disconnects from a hub. + Func BuildDisconnect(Func disconnect); + + /// + /// Wraps a function to be called before a client subscribes to signals belonging to the hub described by the + /// . By default, the will look for attributes on the + /// to help determine if the client is authorized to subscribe to method invocations for the + /// described hub. + /// The function returns true if the client is authorized to subscribe to client-side hub method + /// invocations; false, otherwise. + /// + /// + /// A function that dictates whether or not the client is authorized to connect to the described Hub. + /// + /// + /// A wrapped function that dictates whether or not the client is authorized to connect to the described Hub. + /// + Func BuildAuthorizeConnect(Func authorizeConnect); + + /// + /// Wraps a function that determines which of the groups belonging to the hub described by the + /// the client should be allowed to rejoin. + /// By default, clients will rejoin all the groups they were in prior to reconnecting. + /// + /// A function that determines which groups the client should be allowed to rejoin. + /// A wrapped function that determines which groups the client should be allowed to rejoin. + Func, IList> BuildRejoiningGroups(Func, IList> rejoiningGroups); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs new file mode 100644 index 000000000..6819ed97a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/ReflectionHelper.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public static class ReflectionHelper + { + private static readonly Type[] _excludeTypes = new[] { typeof(Hub), typeof(object) }; + private static readonly Type[] _excludeInterfaces = new[] { typeof(IHub), typeof(IDisposable) }; + + public static IEnumerable GetExportedHubMethods(Type type) + { + if (!typeof(IHub).IsAssignableFrom(type)) + { + return Enumerable.Empty(); + } + + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + var allInterfaceMethods = _excludeInterfaces.SelectMany(i => GetInterfaceMethods(type, i)); + + return methods.Except(allInterfaceMethods).Where(IsValidHubMethod); + + } + + private static bool IsValidHubMethod(MethodInfo methodInfo) + { + return !(_excludeTypes.Contains(methodInfo.GetBaseDefinition().DeclaringType) || + methodInfo.IsSpecialName); + } + + private static IEnumerable GetInterfaceMethods(Type type, Type iface) + { + if (!iface.IsAssignableFrom(type)) + { + return Enumerable.Empty(); + } + + return type.GetInterfaceMap(iface).TargetMethods; + } + + public static TResult GetAttributeValue(ICustomAttributeProvider source, Func valueGetter) + where TAttribute : Attribute + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (valueGetter == null) + { + throw new ArgumentNullException("valueGetter"); + } + + var attributes = source.GetCustomAttributes(typeof(TAttribute), false) + .Cast() + .ToList(); + if (attributes.Any()) + { + return valueGetter(attributes[0]); + } + return default(TResult); + } + + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs new file mode 100644 index 000000000..f003a59ee --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/SignalProxy.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public abstract class SignalProxy : DynamicObject, IClientProxy + { + private readonly IList _exclude; + private readonly string _prefix; + + protected SignalProxy(Func, Task> send, string signal, string hubName, string prefix, IList exclude) + { + Send = send; + Signal = signal; + HubName = hubName; + _prefix = prefix; + _exclude = exclude; + } + + protected Func, Task> Send { get; private set; } + protected string Signal { get; private set; } + protected string HubName { get; private set; } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = null; + return false; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + result = Invoke(binder.Name, args); + return true; + } + + public Task Invoke(string method, params object[] args) + { + var invocation = GetInvocationData(method, args); + + string signal = _prefix + HubName + "." + Signal; + + return Send(signal, invocation, _exclude); + } + + protected virtual ClientHubInvocation GetInvocationData(string method, object[] args) + { + return new ClientHubInvocation + { + Hub = HubName, + Method = method, + Args = args, + Target = Signal + }; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs new file mode 100644 index 000000000..5c7075e31 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/StateChangeTracker.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + /// + /// A change tracking dictionary. + /// + public class StateChangeTracker + { + private readonly IDictionary _values; + // Keep track of everyting that changed since creation + private readonly IDictionary _oldValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public StateChangeTracker() + { + _values = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public StateChangeTracker(IDictionary values) + { + _values = values; + } + + public object this[string key] + { + get + { + object result; + _values.TryGetValue(key, out result); + return DynamicDictionary.Wrap(result); + } + set + { + if (!_oldValues.ContainsKey(key)) + { + object oldValue; + _values.TryGetValue(key, out oldValue); + _oldValues[key] = oldValue; + } + + _values[key] = value; + } + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] + public IDictionary GetChanges() + { + var changes = (from key in _oldValues.Keys + let oldValue = _oldValues[key] + let newValue = _values[key] + where !Object.Equals(oldValue, newValue) + select new + { + Key = key, + Value = newValue + }).ToDictionary(p => p.Key, p => p.Value); + + return changes.Count > 0 ? changes : null; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs b/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs new file mode 100644 index 000000000..8fbac0f32 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Hubs/StatefulSignalProxy.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Hubs +{ + public class StatefulSignalProxy : SignalProxy + { + private readonly StateChangeTracker _tracker; + + public StatefulSignalProxy(Func, Task> send, string signal, string hubName, string prefix, StateChangeTracker tracker) + : base(send, signal, prefix, hubName, ListHelper.Empty) + { + _tracker = tracker; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] + public override bool TrySetMember(SetMemberBinder binder, object value) + { + _tracker[binder.Name] = value; + return true; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The compiler generates calls to invoke this")] + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = _tracker[binder.Name]; + return true; + } + + protected override ClientHubInvocation GetInvocationData(string method, object[] args) + { + return new ClientHubInvocation + { + Hub = HubName, + Method = method, + Args = args, + Target = Signal, + State = _tracker.GetChanges() + }; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IConnection.cs b/src/Microsoft.AspNet.SignalR.Core/IConnection.cs new file mode 100644 index 000000000..a6283c259 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IConnection.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// A communication channel for a and its connections. + /// + public interface IConnection + { + /// + /// The main signal for this connection. This is the main signalr for a . + /// + string DefaultSignal { get; } + + /// + /// Sends a message to connections subscribed to the signal. + /// + /// The message to send. + /// A task that returns when the message has be sent. + Task Send(ConnectionMessage message); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs new file mode 100644 index 000000000..93688901f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IConnectionGroupManager.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Manages groups for a connection and allows sending messages to the group. + /// + public interface IConnectionGroupManager : IGroupManager + { + /// + /// Sends a value to the specified group. + /// + /// The name of the group. + /// The value to send. + /// The list of connection ids to exclude + /// A task that represents when send is complete. + Task Send(string groupName, object value, params string[] excludeConnectionIds); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs b/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs new file mode 100644 index 000000000..a897d2139 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IDependencyResolver.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR +{ + public interface IDependencyResolver : IDisposable + { + object GetService(Type serviceType); + IEnumerable GetServices(Type serviceType); + void Register(Type serviceType, Func activator); + void Register(Type serviceType, IEnumerable> activators); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs b/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs new file mode 100644 index 000000000..3b23e1bb6 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IGroupManager.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Manages groups for a connection. + /// + public interface IGroupManager + { + /// + /// Adds a connection to the specified group. + /// + /// The connection id to add to the group. + /// The name of the group + /// A task that represents the connection id being added to the group. + Task Add(string connectionId, string groupName); + + /// + /// Removes a connection from the specified group. + /// + /// The connection id to remove from the group. + /// The name of the group + /// A task that represents the connection id being removed from the group. + Task Remove(string connectionId, string groupName); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs b/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs new file mode 100644 index 000000000..e5b543d5d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IHubContext.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Provides access to information about a . + /// + public interface IHubContext + { + /// + /// Encapsulates all information about a SignalR connection for an . + /// + IHubConnectionContext Clients { get; } + + /// + /// Gets the the hub. + /// + IGroupManager Groups { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs new file mode 100644 index 000000000..a9b6debda --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IPersistentConnectionContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Provides access to information about a . + /// + public interface IPersistentConnectionContext + { + /// + /// Gets the for the . + /// + IConnection Connection { get; } + + /// + /// Gets the for the . + /// + IConnectionGroupManager Groups { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/IRequest.cs b/src/Microsoft.AspNet.SignalR.Core/IRequest.cs new file mode 100644 index 000000000..496d0e186 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/IRequest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Security.Principal; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Represents a SignalR request + /// + public interface IRequest + { + /// + /// Gets the url for this request. + /// + Uri Url { get; } + + /// + /// Gets the querystring for this request. + /// + NameValueCollection QueryString { get; } + + /// + /// Gets the headers for this request. + /// + NameValueCollection Headers { get; } + + /// + /// Gets the form for this request. + /// + NameValueCollection Form { get; } + + /// + /// Gets the cookies for this request. + /// + IDictionary Cookies { get; } + + /// + /// Gets security information for the current HTTP request. + /// + IPrincipal User { get; } + + /// + /// Gets state for the current HTTP request. + /// + IDictionary Items { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs new file mode 100644 index 000000000..18479ec4c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/AckHandler.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public class AckHandler : IAckHandler, IDisposable + { + private readonly ConcurrentDictionary _acks = new ConcurrentDictionary(); + + // REVIEW: Consider making this pluggable + private readonly TimeSpan _ackThreshold; + + // REVIEW: Consider moving this logic to the transport heartbeat + private Timer _timer; + + public AckHandler() + : this(completeAcksOnTimeout: true, + ackThreshold: TimeSpan.FromSeconds(30), + ackInterval: TimeSpan.FromSeconds(5)) + { + } + + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Acks", Justification = "Ack is a well known term")] + public AckHandler(bool completeAcksOnTimeout, TimeSpan ackThreshold, TimeSpan ackInterval) + { + if (completeAcksOnTimeout) + { + _timer = new Timer(_ => CheckAcks(), state: null, dueTime: ackInterval, period: ackInterval); + } + + _ackThreshold = ackThreshold; + } + + public Task CreateAck(string id) + { + return _acks.GetOrAdd(id, _ => new AckInfo()).Tcs.Task; + } + + public bool TriggerAck(string id) + { + AckInfo info; + if (_acks.TryRemove(id, out info)) + { + info.Tcs.TrySetResult(null); + return true; + } + + return false; + } + + private void CheckAcks() + { + foreach (var pair in _acks) + { + TimeSpan elapsed = DateTime.UtcNow - pair.Value.Created; + if (elapsed > _ackThreshold) + { + AckInfo info; + if (_acks.TryRemove(pair.Key, out info)) + { + // If we have a pending ack for longer than the threshold + // cancel it. + info.Tcs.TrySetCanceled(); + } + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_timer != null) + { + _timer.Dispose(); + } + + // Trip all pending acks + foreach (var pair in _acks) + { + AckInfo info; + if (_acks.TryRemove(pair.Key, out info)) + { + info.Tcs.TrySetCanceled(); + } + } + } + } + + public void Dispose() + { + Dispose(true); + } + + private class AckInfo + { + public TaskCompletionSource Tcs { get; private set; } + public DateTime Created { get; private set; } + + public AckInfo() + { + Tcs = new TaskCompletionSource(); + Created = DateTime.UtcNow; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs new file mode 100644 index 000000000..08dfb1f97 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ArraySegmentTextReader.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.IO; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public class ArraySegmentTextReader : TextReader + { + private readonly ArraySegment _buffer; + private readonly Encoding _encoding; + private int _offset; + + public ArraySegmentTextReader(ArraySegment buffer, Encoding encoding) + { + _buffer = buffer; + _encoding = encoding; + _offset = _buffer.Offset; + } + + public override int Read(char[] buffer, int index, int count) + { + int bytesCount = _encoding.GetByteCount(buffer, index, count); + int bytesToRead = Math.Min(_buffer.Count - _offset, bytesCount); + + int read = _encoding.GetChars(_buffer.Array, _offset, bytesToRead, buffer, index); + _offset += bytesToRead; + + return read; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs new file mode 100644 index 000000000..2cf39d536 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/BufferTextWriter.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; +using Microsoft.AspNet.SignalR.Hosting; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// TextWriter implementation over a write delegate optimized for writing in small chunks + /// we don't need to write to a long lived buffer. This saves massive amounts of memory + /// as the number of connections grows. + /// + internal unsafe class BufferTextWriter : TextWriter, IBinaryWriter + { + private readonly Encoding _encoding; + + private readonly Action, object> _write; + private readonly object _writeState; + private readonly bool _reuseBuffers; + + private ChunkedWriter _writer; + private int _bufferSize; + + public BufferTextWriter(IResponse response) : + this((data, state) => ((IResponse)state).Write(data), response, reuseBuffers: true, bufferSize: 128) + { + + } + + public BufferTextWriter(IWebSocket socket) : + this((data, state) => ((IWebSocket)state).SendChunk(data), socket, reuseBuffers: false, bufferSize: 128) + { + + } + + [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.TextWriter.#ctor", Justification = "It won't be used")] + public BufferTextWriter(Action, object> write, object state, bool reuseBuffers, int bufferSize) + { + _write = write; + _writeState = state; + _encoding = new UTF8Encoding(); + _reuseBuffers = reuseBuffers; + _bufferSize = bufferSize; + } + + private ChunkedWriter Writer + { + get + { + if (_writer == null) + { + _writer = new ChunkedWriter(_write, _writeState, _bufferSize, _encoding, _reuseBuffers); + } + + return _writer; + } + } + + public override Encoding Encoding + { + get { return _encoding; } + } + + public override void Write(string value) + { + Writer.Write(value); + } + + public override void WriteLine(string value) + { + Writer.Write(value); + } + + public override void Write(char value) + { + Writer.Write(value); + } + + public void Write(ArraySegment data) + { + Writer.Write(data); + } + + public override void Flush() + { + Writer.Flush(); + } + + private class ChunkedWriter + { + private int _charPos; + private int _charLen; + + private readonly Encoder _encoder; + private readonly char[] _charBuffer; + private readonly byte[] _byteBuffer; + private readonly Action, object> _write; + private readonly object _writeState; + + public ChunkedWriter(Action, object> write, object state, int chunkSize, Encoding encoding, bool reuseBuffers) + { + _charLen = chunkSize; + _charBuffer = new char[chunkSize]; + _write = write; + _writeState = state; + _encoder = encoding.GetEncoder(); + + if (reuseBuffers) + { + _byteBuffer = new byte[encoding.GetMaxByteCount(chunkSize)]; + } + } + + public void Write(char value) + { + if (_charPos == _charLen) + { + Flush(flushEncoder: false); + } + + _charBuffer[_charPos++] = value; + } + + public void Write(string value) + { + int length = value.Length; + int sourceIndex = 0; + + while (length > 0) + { + if (_charPos == _charLen) + { + Flush(flushEncoder: false); + } + + int count = _charLen - _charPos; + if (count > length) + { + count = length; + } + + value.CopyTo(sourceIndex, _charBuffer, _charPos, count); + _charPos += count; + sourceIndex += count; + length -= count; + } + } + + public void Write(ArraySegment data) + { + Flush(); + _write(data, _writeState); + } + + public void Flush() + { + Flush(flushEncoder: true); + } + + private void Flush(bool flushEncoder) + { + // If it's safe to reuse the buffer then do so + if (_byteBuffer != null) + { + Flush(_byteBuffer, flushEncoder); + } + else + { + // Allocate a byte array of the right size for this char buffer + int byteCount = _encoder.GetByteCount(_charBuffer, 0, _charPos, flush: false); + var byteBuffer = new byte[byteCount]; + Flush(byteBuffer, flushEncoder); + } + } + + private void Flush(byte[] byteBuffer, bool flushEncoder) + { + int count = _encoder.GetBytes(_charBuffer, 0, _charPos, byteBuffer, 0, flush: flushEncoder); + + _charPos = 0; + + if (count > 0) + { + _write(new ArraySegment(byteBuffer, 0, count), _writeState); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs new file mode 100644 index 000000000..71736b3df --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/CancellationTokenExtensions.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal static class CancellationTokenExtensions + { + public static IDisposable SafeRegister(this CancellationToken cancellationToken, Action callback, object state) + { + var callbackWrapper = new CancellationCallbackWrapper(callback, state); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + CancellationTokenRegistration registration = cancellationToken.Register(s => Cancel(s), + callbackWrapper, + useSynchronizationContext: false); + + var disposeCancellationState = new DiposeCancellationState(callbackWrapper, registration); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return new DisposableAction(s => Dispose(s), disposeCancellationState); + } + + private static void Cancel(object state) + { + ((CancellationCallbackWrapper)state).TryInvoke(); + } + + private static void Dispose(object state) + { + ((DiposeCancellationState)state).TryDispose(); + } + + private class DiposeCancellationState + { + private readonly CancellationCallbackWrapper _callbackWrapper; + private readonly CancellationTokenRegistration _registration; + + public DiposeCancellationState(CancellationCallbackWrapper callbackWrapper, CancellationTokenRegistration registration) + { + _callbackWrapper = callbackWrapper; + _registration = registration; + } + + public void TryDispose() + { + // This normally waits until the callback is finished invoked but we don't care + if (_callbackWrapper.TrySetInvoked()) + { + // Bug #1549, .NET 4.0 has a bug where this throws if the CTS + _registration.Dispose(); + } + } + } + + private class CancellationCallbackWrapper + { + private readonly Action _callback; + private readonly object _state; + private int _callbackInvoked; + + public CancellationCallbackWrapper(Action callback, object state) + { + _callback = callback; + _state = state; + } + + public bool TrySetInvoked() + { + return Interlocked.Exchange(ref _callbackInvoked, 1) == 0; + } + + public void TryInvoke() + { + if (TrySetInvoked()) + { + _callback(_state); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs new file mode 100644 index 000000000..bb0782de6 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; +using Microsoft.AspNet.SignalR.Tracing; +using Microsoft.AspNet.SignalR.Transports; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public class Connection : IConnection, ITransportConnection, ISubscriber + { + private readonly IMessageBus _bus; + private readonly IJsonSerializer _serializer; + private readonly string _baseSignal; + private readonly string _connectionId; + private readonly IList _signals; + private readonly DiffSet _groups; + private readonly IPerformanceCounterManager _counters; + + private bool _disconnected; + private bool _aborted; + private readonly TraceSource _traceSource; + private readonly IAckHandler _ackHandler; + private readonly IProtectedData _protectedData; + + public Connection(IMessageBus newMessageBus, + IJsonSerializer jsonSerializer, + string baseSignal, + string connectionId, + IList signals, + IList groups, + ITraceManager traceManager, + IAckHandler ackHandler, + IPerformanceCounterManager performanceCounterManager, + IProtectedData protectedData) + { + if (traceManager == null) + { + throw new ArgumentNullException("traceManager"); + } + + _bus = newMessageBus; + _serializer = jsonSerializer; + _baseSignal = baseSignal; + _connectionId = connectionId; + _signals = new List(signals.Concat(groups)); + _groups = new DiffSet(groups); + _traceSource = traceManager["SignalR.Connection"]; + _ackHandler = ackHandler; + _counters = performanceCounterManager; + _protectedData = protectedData; + } + + public string DefaultSignal + { + get + { + return _baseSignal; + } + } + + IList ISubscriber.EventKeys + { + get + { + return _signals; + } + } + + public event Action EventKeyAdded; + + public event Action EventKeyRemoved; + + public Action WriteCursor { get; set; } + + public string Identity + { + get + { + return _connectionId; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Used for debugging purposes.")] + private TraceSource Trace + { + get + { + return _traceSource; + } + } + + public Subscription Subscription + { + get; + set; + } + + public Task Send(ConnectionMessage message) + { + Message busMessage = CreateMessage(message.Signal, message.Value); + + if (message.ExcludedSignals != null) + { + busMessage.Filter = String.Join("|", message.ExcludedSignals); + } + + if (busMessage.WaitForAck) + { + Task ackTask = _ackHandler.CreateAck(busMessage.CommandId); + return _bus.Publish(busMessage).Then(task => task, ackTask); + } + + return _bus.Publish(busMessage); + } + + private Message CreateMessage(string key, object value) + { + var command = value as Command; + + ArraySegment messageBuffer = GetMessageBuffer(value); + + var message = new Message(_connectionId, key, messageBuffer); + + if (command != null) + { + // Set the command id + message.CommandId = command.Id; + message.WaitForAck = command.WaitForAck; + } + + return message; + } + + private ArraySegment GetMessageBuffer(object value) + { + using (var stream = new MemoryStream(128)) + { + var bufferWriter = new BufferTextWriter((buffer, state) => + { + ((MemoryStream)state).Write(buffer.Array, buffer.Offset, buffer.Count); + }, + stream, + reuseBuffers: true, + bufferSize: 1024); + + using (bufferWriter) + { + _serializer.Serialize(value, bufferWriter); + bufferWriter.Flush(); + + return new ArraySegment(stream.ToArray()); + } + } + } + + public IDisposable Receive(string messageId, Func> callback, int maxMessages, object state) + { + var receiveContext = new ReceiveContext(this, callback, state); + + return _bus.Subscribe(this, + messageId, + (result, s) => MessageBusCallback(result, s), + maxMessages, + receiveContext); + } + + private static Task MessageBusCallback(MessageResult result, object state) + { + var context = (ReceiveContext)state; + + return context.InvokeCallback(result); + } + + private PersistentResponse GetResponse(MessageResult result) + { + // Do a single sweep through the results to process commands and extract values + ProcessResults(result); + + Debug.Assert(WriteCursor != null, "Unable to resolve the cursor since the method is null"); + + var response = new PersistentResponse(ExcludeMessage, WriteCursor); + response.Terminal = result.Terminal; + + if (!result.Terminal) + { + // Only set these properties if the message isn't terminal + response.Messages = result.Messages; + response.Disconnect = _disconnected; + response.Aborted = _aborted; + response.TotalCount = result.TotalCount; + } + + PopulateResponseState(response); + + _counters.ConnectionMessagesReceivedTotal.IncrementBy(result.TotalCount); + _counters.ConnectionMessagesReceivedPerSec.IncrementBy(result.TotalCount); + + return response; + } + + private bool ExcludeMessage(Message message) + { + if (String.IsNullOrEmpty(message.Filter)) + { + return false; + } + + string[] exclude = message.Filter.Split('|'); + + return exclude.Any(signal => Identity.Equals(signal, StringComparison.OrdinalIgnoreCase) || + _signals.Contains(signal) || + _groups.Contains(signal)); + } + + private void ProcessResults(MessageResult result) + { + result.Messages.Enumerate(message => message.IsAck || message.IsCommand, + (state, message) => + { + if (message.IsAck) + { + _ackHandler.TriggerAck(message.CommandId); + } + else if (message.IsCommand) + { + var command = _serializer.Parse(message.Value, message.Encoding); + ProcessCommand(command); + + // Only send the ack if this command is waiting for it + if (message.WaitForAck) + { + // If we're on the same box and there's a pending ack for this command then + // just trip it + if (!_ackHandler.TriggerAck(message.CommandId)) + { + _bus.Ack(_connectionId, message.CommandId).Catch(); + } + } + } + }, null); + } + + private void ProcessCommand(Command command) + { + switch (command.CommandType) + { + case CommandType.AddToGroup: + { + var name = command.Value; + + if (EventKeyAdded != null) + { + _groups.Add(name); + EventKeyAdded(this, name); + } + } + break; + case CommandType.RemoveFromGroup: + { + var name = command.Value; + + if (EventKeyRemoved != null) + { + _groups.Remove(name); + EventKeyRemoved(this, name); + } + } + break; + case CommandType.Disconnect: + _disconnected = true; + break; + case CommandType.Abort: + _aborted = true; + break; + } + } + + private void PopulateResponseState(PersistentResponse response) + { + PopulateResponseState(response, _groups, _serializer, _protectedData, _connectionId); + } + + internal static void PopulateResponseState(PersistentResponse response, + DiffSet groupSet, + IJsonSerializer serializer, + IProtectedData protectedData, + string connectionId) + { + bool anyChanges = groupSet.DetectChanges(); + + if (anyChanges) + { + // Create a protected payload of the sorted list + IEnumerable groups = groupSet.GetSnapshot(); + + // Remove group prefixes before any thing goes over the wire + string groupsString = connectionId + ':' + serializer.Stringify(PrefixHelper.RemoveGroupPrefixes(groups)); ; + + // The groups token + response.GroupsToken = protectedData.Protect(groupsString, Purposes.Groups); + } + } + + private class ReceiveContext + { + private readonly Connection _connection; + private readonly Func> _callback; + private readonly object _callbackState; + + public ReceiveContext(Connection connection, Func> callback, object callbackState) + { + _connection = connection; + _callback = callback; + _callbackState = callbackState; + } + + public Task InvokeCallback(MessageResult result) + { + var response = _connection.GetResponse(result); + + return _callback(response, _callbackState); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs new file mode 100644 index 000000000..994b3a8f2 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ConnectionManager.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hubs; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Default implementation. + /// + public class ConnectionManager : IConnectionManager + { + private readonly IDependencyResolver _resolver; + private readonly IPerformanceCounterManager _counters; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public ConnectionManager(IDependencyResolver resolver) + { + _resolver = resolver; + _counters = _resolver.Resolve(); + } + + /// + /// Returns a for the . + /// + /// Type of the + /// A for the . + public IPersistentConnectionContext GetConnectionContext() where T : PersistentConnection + { + return GetConnection(typeof(T)); + } + + /// + /// Returns a for the . + /// + /// Type of the + /// A for the . + public IPersistentConnectionContext GetConnection(Type type) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + string rawConnectionName = type.FullName; + string connectionName = PrefixHelper.GetPersistentConnectionName(rawConnectionName); + IConnection connection = GetConnectionCore(connectionName); + + return new PersistentConnectionContext(connection, new GroupManager(connection, PrefixHelper.GetPersistentConnectionGroupName(rawConnectionName))); + } + + /// + /// Returns a for the specified . + /// + /// Type of the + /// a for the specified + public IHubContext GetHubContext() where T : IHub + { + return GetHubContext(typeof(T).GetHubName()); + } + + /// + /// Returns a for the specified hub. + /// + /// Name of the hub + /// a for the specified hub + public IHubContext GetHubContext(string hubName) + { + var connection = GetConnectionCore(connectionName: null); + var hubManager = _resolver.Resolve(); + var pipelineInvoker = _resolver.Resolve(); + + hubManager.EnsureHub(hubName, + _counters.ErrorsHubResolutionTotal, + _counters.ErrorsHubResolutionPerSec, + _counters.ErrorsAllTotal, + _counters.ErrorsAllPerSec); + + Func, Task> send = (signal, value, exclude) => pipelineInvoker.Send(new HubOutgoingInvokerContext(connection, signal, value, exclude)); + + return new HubContext(send, hubName, connection); + } + + internal Connection GetConnectionCore(string connectionName) + { + IList signals = connectionName == null ? ListHelper.Empty : new[] { connectionName }; + + // Give this a unique id + var connectionId = Guid.NewGuid().ToString(); + return new Connection(_resolver.Resolve(), + _resolver.Resolve(), + connectionName, + connectionId, + signals, + ListHelper.Empty, + _resolver.Resolve(), + _resolver.Resolve(), + _resolver.Resolve(), + _resolver.Resolve()); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs new file mode 100644 index 000000000..686f33abf --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DefaultProtectedData.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public class DefaultProtectedData : IProtectedData + { + private static readonly UTF8Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + public string Protect(string data, string purpose) + { + byte[] purposeBytes = _encoding.GetBytes(purpose); + + byte[] unprotectedBytes = _encoding.GetBytes(data); + + byte[] protectedBytes = ProtectedData.Protect(unprotectedBytes, purposeBytes, DataProtectionScope.CurrentUser); + + return Convert.ToBase64String(protectedBytes); + } + + public string Unprotect(string protectedValue, string purpose) + { + byte[] purposeBytes = _encoding.GetBytes(purpose); + + byte[] protectedBytes = Convert.FromBase64String(protectedValue); + + byte[] unprotectedBytes = ProtectedData.Unprotect(protectedBytes, purposeBytes, DataProtectionScope.CurrentUser); + + return _encoding.GetString(unprotectedBytes); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs new file mode 100644 index 000000000..f7a92b81e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffPair.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal struct DiffPair + { + public ICollection Added; + public ICollection Removed; + + public bool AnyChanges + { + get + { + return Added.Count > 0 || Removed.Count > 0; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs new file mode 100644 index 000000000..0d322bf13 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DiffSet.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class DiffSet + { + private readonly HashSet _items; + private readonly HashSet _addedItems; + private readonly HashSet _removedItems; + + public DiffSet(IEnumerable items) + { + _addedItems = new HashSet(); + _removedItems = new HashSet(); + + _items = new HashSet(items); + } + + public bool Add(T item) + { + if (_items.Add(item)) + { + if (!_removedItems.Remove(item)) + { + _addedItems.Add(item); + } + return true; + } + return false; + } + + public bool Remove(T item) + { + if (_items.Remove(item)) + { + if (!_addedItems.Remove(item)) + { + _removedItems.Add(item); + } + return true; + } + return false; + } + + public bool Contains(T item) + { + return _items.Contains(item); + } + + public ICollection GetSnapshot() + { + return _items; + } + + public bool DetectChanges() + { + bool anyChanges = _addedItems.Count > 0 || _removedItems.Count > 0; + _addedItems.Clear(); + _removedItems.Clear(); + return anyChanges; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs new file mode 100644 index 000000000..ab336d604 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/DisposableAction.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class DisposableAction : IDisposable + { + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "The client projects use this.")] + public static readonly DisposableAction Empty = new DisposableAction(() => { }); + + private Action _action; + private readonly object _state; + + public DisposableAction(Action action) + : this(state => ((Action)state).Invoke(), state: action) + { + + } + + public DisposableAction(Action action, object state) + { + _action = action; + _state = state; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Interlocked.Exchange(ref _action, (state) => { }).Invoke(_state); + } + } + + public void Dispose() + { + Dispose(true); + } + } + +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs new file mode 100644 index 000000000..13273943a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Disposer.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Helper class to manage disposing a resource at an arbirtary time + /// + internal class Disposer : IDisposable + { + private static readonly object _disposedSentinel = new object(); + + private object _disposable; + + public void Set(IDisposable disposable) + { + if (disposable == null) + { + throw new ArgumentNullException("disposable"); + } + + object originalFieldValue = Interlocked.CompareExchange(ref _disposable, disposable, null); + if (originalFieldValue == null) + { + // this is the first call to Set() and Dispose() hasn't yet been called; do nothing + } + else if (originalFieldValue == _disposedSentinel) + { + // Dispose() has already been called, so we need to dispose of the object that was just added + disposable.Dispose(); + } + else + { +#if !NET35 && !SILVERLIGHT && !NETFX_CORE + // Set has been called multiple times, fail + Debug.Fail("Multiple calls to Disposer.Set(IDisposable) without calling Disposer.Dispose()"); +#endif + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + var disposable = Interlocked.Exchange(ref _disposable, _disposedSentinel) as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs new file mode 100644 index 000000000..5545f2f81 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ExceptionsExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal static class ExceptionsExtensions + { + internal static Exception Unwrap(this Exception ex) + { + if (ex == null) + { + return null; + } + + var next = ex.GetBaseException(); + while (next.InnerException != null) + { + // On mono GetBaseException() doesn't seem to do anything + // so just walk the inner exception chain. + next = next.InnerException; + } + + return next; + } + } +} + diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs new file mode 100644 index 000000000..bbdc3c36f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IAckHandler.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public interface IAckHandler + { + Task CreateAck(string id); + + bool TriggerAck(string id); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs new file mode 100644 index 000000000..06be2531d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IBinaryWriter.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Implemented on anything that has the ability to write raw binary data + /// + public interface IBinaryWriter + { + void Write(ArraySegment data); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs new file mode 100644 index 000000000..bd783d12e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IConnectionManager.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Hubs; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Provides access to hubs and persistent connections references. + /// + public interface IConnectionManager + { + /// + /// Returns a for the specified . + /// + /// Type of the + /// a for the specified + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The hub type needs to be specified")] + IHubContext GetHubContext() where T : IHub; + + /// + /// Returns a for the specified hub. + /// + /// Name of the hub + /// a for the specified hub + IHubContext GetHubContext(string hubName); + + /// + /// Returns a for the . + /// + /// Type of the + /// A for the . + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The connection type needs to be specified")] + IPersistentConnectionContext GetConnectionContext() where T : PersistentConnection; + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs new file mode 100644 index 000000000..3f90659e4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounter.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public interface IPerformanceCounter + { + string CounterName { get; } + long Decrement(); + long Increment(); + long IncrementBy(long value); + CounterSample NextSample(); + long RawValue { get; set; } + void Close(); + void RemoveInstance(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs new file mode 100644 index 000000000..84293c354 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IPerformanceCounterManager.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Provides access to performance counters. + /// + public interface IPerformanceCounterManager + { + /// + /// Initializes the performance counters. + /// + /// The host instance name. + /// The CancellationToken representing the host shutdown. + void Initialize(string instanceName, CancellationToken hostShutdownToken); + + /// + /// Loads a performance counter. + /// + /// The category name. + /// The counter name. + /// The instance name. + /// Whether the counter is read-only. + IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly); + + /// + /// Gets the performance counter representing the total number of connection Connect events since the application was started. + /// + IPerformanceCounter ConnectionsConnected { get; } + + /// + /// Gets the performance counter representing the total number of connection Reconnect events since the application was started. + /// + IPerformanceCounter ConnectionsReconnected { get; } + + /// + /// Gets the performance counter representing the total number of connection Disconnect events since the application was started. + /// + IPerformanceCounter ConnectionsDisconnected { get; } + + /// + /// Gets the performance counter representing the number of connections currently connected. + /// + IPerformanceCounter ConnectionsCurrent { get; } + + /// + /// Gets the performance counter representing the total number of messages received by connections (server to client) since the application was started. + /// + IPerformanceCounter ConnectionMessagesReceivedTotal { get; } + + /// + /// Gets the performance counter representing the total number of messages received by connections (server to client) since the application was started. + /// + IPerformanceCounter ConnectionMessagesSentTotal { get; } + + /// + /// Gets the performance counter representing the number of messages received by connections (server to client) per second. + /// + IPerformanceCounter ConnectionMessagesReceivedPerSec { get; } + + /// + /// Gets the performance counter representing the number of messages sent by connections (client to server) per second. + /// + IPerformanceCounter ConnectionMessagesSentPerSec { get; } + + /// + /// Gets the performance counter representing the total number of messages received by subscribers since the application was started. + /// + IPerformanceCounter MessageBusMessagesReceivedTotal { get; } + + /// + /// Gets the performance counter representing the number of messages received by a subscribers per second. + /// + IPerformanceCounter MessageBusMessagesReceivedPerSec { get; } + + /// + /// Gets the performance counter representing the number of messages received by the scaleout message bus per second. + /// + IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec { get; } + + /// + /// Gets the performance counter representing the total number of messages published to the message bus since the application was started. + /// + IPerformanceCounter MessageBusMessagesPublishedTotal { get; } + + /// + /// Gets the performance counter representing the number of messages published to the message bus per second. + /// + IPerformanceCounter MessageBusMessagesPublishedPerSec { get; } + + /// + /// Gets the performance counter representing the current number of subscribers to the message bus. + /// + IPerformanceCounter MessageBusSubscribersCurrent { get; } + + /// + /// Gets the performance counter representing the total number of subscribers to the message bus since the application was started. + /// + IPerformanceCounter MessageBusSubscribersTotal { get; } + + /// + /// Gets the performance counter representing the number of new subscribers to the message bus per second. + /// + IPerformanceCounter MessageBusSubscribersPerSec { get; } + + /// + /// Gets the performance counter representing the number of workers allocated to deliver messages in the message bus. + /// + IPerformanceCounter MessageBusAllocatedWorkers { get; } + + /// + /// Gets the performance counter representing the number of workers currently busy delivering messages in the message bus. + /// + IPerformanceCounter MessageBusBusyWorkers { get; } + + /// + /// Gets the performance counter representing representing the current number of topics in the message bus. + /// + IPerformanceCounter MessageBusTopicsCurrent { get; } + + /// + /// Gets the performance counter representing the total number of all errors processed since the application was started. + /// + IPerformanceCounter ErrorsAllTotal { get; } + + /// + /// Gets the performance counter representing the number of all errors processed per second. + /// + IPerformanceCounter ErrorsAllPerSec { get; } + + /// + /// Gets the performance counter representing the total number of hub resolution errors processed since the application was started. + /// + IPerformanceCounter ErrorsHubResolutionTotal { get; } + + /// + /// Gets the performance counter representing the number of hub resolution errors per second. + /// + IPerformanceCounter ErrorsHubResolutionPerSec { get; } + + /// + /// Gets the performance counter representing the total number of hub invocation errors processed since the application was started. + /// + IPerformanceCounter ErrorsHubInvocationTotal { get; } + + /// + /// Gets the performance counter representing the number of hub invocation errors per second. + /// + IPerformanceCounter ErrorsHubInvocationPerSec { get; } + + /// + /// Gets the performance counter representing the total number of transport errors processed since the application was started. + /// + IPerformanceCounter ErrorsTransportTotal { get; } + + /// + /// Gets the performance counter representing the number of transport errors per second. + /// + IPerformanceCounter ErrorsTransportPerSec { get; } + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider. + /// + IPerformanceCounter ScaleoutStreamCountTotal { get; } + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the open state. + /// + IPerformanceCounter ScaleoutStreamCountOpen { get; } + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the buffering state. + /// + IPerformanceCounter ScaleoutStreamCountBuffering { get; } + + /// + /// Gets the performance counter representing the total number of scaleout errors since the application was started. + /// + IPerformanceCounter ScaleoutErrorsTotal { get; } + + /// + /// Gets the performance counter representing the number of scaleout errors per second. + /// + IPerformanceCounter ScaleoutErrorsPerSec { get; } + + /// + /// Gets the performance counter representing the current scaleout send queue length. + /// + IPerformanceCounter ScaleoutSendQueueLength { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs new file mode 100644 index 000000000..8f46d2145 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IProtectedData.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public interface IProtectedData + { + string Protect(string data, string purpose); + string Unprotect(string protectedValue, string purpose); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs new file mode 100644 index 000000000..9db7f16fc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerCommandHandler.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Handles commands from server to server. + /// + internal interface IServerCommandHandler + { + /// + /// Sends a command to all connected servers. + /// + /// + /// + Task SendCommand(ServerCommand command); + + /// + /// Gets or sets a callback that is invoked when a command is received. + /// + Action Command { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs new file mode 100644 index 000000000..a7637b70a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IServerIdManager.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Generates a server id + /// + public interface IServerIdManager + { + /// + /// The id of the server. + /// + string ServerId { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs new file mode 100644 index 000000000..6ff5a7941 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/IStringMinifier.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public interface IStringMinifier + { + /// + /// Minifies a string in a way that can be reversed by this instance of . + /// + /// The string to be minified + /// A minified representation of the without the following characters:,|\ + string Minify(string value); + + /// + /// Reverses a call that was executed at least once previously on this instance of + /// without any subsequent calls to sharing the + /// same argument as the call that returned . + /// + /// + /// A minified string that was returned by a previous call to . + /// + /// + /// The argument of all previous calls to that returned . + /// If every call to on this instance of has never + /// returned or if the most recent call to that did + /// return was followed by a call to sharing + /// the same argument, may return null but must not throw. + /// + string Unminify(string value); + + /// + /// A call to this function indicates that any future attempt to unminify strings that were previously minified + /// from may be met with a null return value. This provides an opportunity clean up + /// any internal data structures that reference . + /// + /// The string that may have previously have been minified. + void RemoveUnminified(string value); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs new file mode 100644 index 000000000..3f4044dd7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/InterlockedHelper.cs @@ -0,0 +1,16 @@ +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + + public static class InterlockedHelper + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification="This is an interlocked helper...")] + public static bool CompareExchangeOr(ref int location, int value, int comparandA, int comparandB) + { + return Interlocked.CompareExchange(ref location, value, comparandA) == comparandA || + Interlocked.CompareExchange(ref location, value, comparandB) == comparandB; + } + } + +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs new file mode 100644 index 000000000..12e8b8dce --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ListHelper.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class ListHelper + { + public static readonly IList Empty = new ReadOnlyCollection(new List()); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs new file mode 100644 index 000000000..ab3daade7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/NoOpPerformanceCounter.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class NoOpPerformanceCounter : IPerformanceCounter + { + public string CounterName + { + get + { + return GetType().Name; + } + } + + public long Decrement() + { + return 0; + } + + public long Increment() + { + return 0; + } + + public long IncrementBy(long value) + { + return 0; + } + + public long RawValue + { + get { return 0; } + set { } + } + + public void Close() + { + + } + + public void RemoveInstance() + { + + } + + public CounterSample NextSample() + { + return CounterSample.Empty; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs new file mode 100644 index 000000000..7a2cb0682 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple=false)] + internal sealed class PerformanceCounterAttribute : Attribute + { + public string Name { get; set; } + public string Description { get; set; } + public PerformanceCounterType CounterType { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs new file mode 100644 index 000000000..d7d8e7550 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterManager.cs @@ -0,0 +1,420 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +#if !UTILS +using Microsoft.AspNet.SignalR.Tracing; +#endif + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Manages performance counters using Windows performance counters. + /// + public class PerformanceCounterManager : IPerformanceCounterManager + { + /// + /// The performance counter category name for SignalR counters. + /// + public const string CategoryName = "SignalR"; + + private readonly static PropertyInfo[] _counterProperties = GetCounterPropertyInfo(); + private readonly static IPerformanceCounter _noOpCounter = new NoOpPerformanceCounter(); + private volatile bool _initialized; + private object _initLocker = new object(); + +#if !UTILS + private readonly TraceSource _trace; + + public PerformanceCounterManager(DefaultDependencyResolver resolver) + : this(resolver.Resolve()) + { + + } + + /// + /// Creates a new instance. + /// + public PerformanceCounterManager(ITraceManager traceManager) + : this() + { + if (traceManager == null) + { + throw new ArgumentNullException("traceManager"); + } + + _trace = traceManager["SignalR.PerformanceCounterManager"]; + } +#endif + + public PerformanceCounterManager() + { + InitNoOpCounters(); + } + + /// + /// Gets the performance counter representing the total number of connection Connect events since the application was started. + /// + [PerformanceCounter(Name = "Connections Connected", Description = "The total number of connection Connect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ConnectionsConnected { get; private set; } + + /// + /// Gets the performance counter representing the total number of connection Reconnect events since the application was started. + /// + [PerformanceCounter(Name = "Connections Reconnected", Description = "The total number of connection Reconnect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ConnectionsReconnected { get; private set; } + + /// + /// Gets the performance counter representing the total number of connection Disconnect events since the application was started. + /// + [PerformanceCounter(Name = "Connections Disconnected", Description = "The total number of connection Disconnect events since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ConnectionsDisconnected { get; private set; } + + /// + /// Gets the performance counter representing the number of connections currently connected. + /// + [PerformanceCounter(Name = "Connections Current", Description = "The number of connections currently connected.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ConnectionsCurrent { get; private set; } + + /// + /// Gets the performance counter representing the toal number of messages received by connections (server to client) since the application was started. + /// + [PerformanceCounter(Name = "Connection Messages Received Total", Description = "The toal number of messages received by connections (server to client) since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] + public IPerformanceCounter ConnectionMessagesReceivedTotal { get; private set; } + + /// + /// Gets the performance counter representing the total number of messages sent by connections (client to server) since the application was started. + /// + [PerformanceCounter(Name = "Connection Messages Sent Total", Description = "The total number of messages sent by connections (client to server) since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] + public IPerformanceCounter ConnectionMessagesSentTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of messages received by connections (server to client) per second. + /// + [PerformanceCounter(Name = "Connection Messages Received/Sec", Description = "The number of messages received by connections (server to client) per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ConnectionMessagesReceivedPerSec { get; private set; } + + /// + /// Gets the performance counter representing the number of messages sent by connections (client to server) per second. + /// + [PerformanceCounter(Name = "Connection Messages Sent/Sec", Description = "The number of messages sent by connections (client to server) per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ConnectionMessagesSentPerSec { get; private set; } + + /// + /// Gets the performance counter representing the total number of messages received by subscribers since the application was started. + /// + [PerformanceCounter(Name = "Message Bus Messages Received Total", Description = "The total number of messages received by subscribers since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] + public IPerformanceCounter MessageBusMessagesReceivedTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of messages received by a subscribers per second. + /// + [PerformanceCounter(Name = "Message Bus Messages Received/Sec", Description = "The number of messages received by subscribers per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter MessageBusMessagesReceivedPerSec { get; private set; } + + /// + /// Gets the performance counter representing the number of messages received by the scaleout message bus per second. + /// + [PerformanceCounter(Name = "Scaleout Message Bus Messages Received/Sec", Description = "The number of messages received by the scaleout message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec { get; private set; } + + + /// + /// Gets the performance counter representing the total number of messages published to the message bus since the application was started. + /// + [PerformanceCounter(Name = "Messages Bus Messages Published Total", Description = "The total number of messages published to the message bus since the application was started.", CounterType = PerformanceCounterType.NumberOfItems64)] + public IPerformanceCounter MessageBusMessagesPublishedTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of messages published to the message bus per second. + /// + [PerformanceCounter(Name = "Messages Bus Messages Published/Sec", Description = "The number of messages published to the message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter MessageBusMessagesPublishedPerSec { get; private set; } + + /// + /// Gets the performance counter representing the current number of subscribers to the message bus. + /// + [PerformanceCounter(Name = "Message Bus Subscribers Current", Description = "The current number of subscribers to the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter MessageBusSubscribersCurrent { get; private set; } + + /// + /// Gets the performance counter representing the total number of subscribers to the message bus since the application was started. + /// + [PerformanceCounter(Name = "Message Bus Subscribers Total", Description = "The total number of subscribers to the message bus since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter MessageBusSubscribersTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of new subscribers to the message bus per second. + /// + [PerformanceCounter(Name = "Message Bus Subscribers/Sec", Description = "The number of new subscribers to the message bus per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter MessageBusSubscribersPerSec { get; private set; } + + /// + /// Gets the performance counter representing the number of workers allocated to deliver messages in the message bus. + /// + [PerformanceCounter(Name = "Message Bus Allocated Workers", Description = "The number of workers allocated to deliver messages in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter MessageBusAllocatedWorkers { get; private set; } + + /// + /// Gets the performance counter representing the number of workers currently busy delivering messages in the message bus. + /// + [PerformanceCounter(Name = "Message Bus Busy Workers", Description = "The number of workers currently busy delivering messages in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter MessageBusBusyWorkers { get; private set; } + + /// + /// Gets the performance counter representing representing the current number of topics in the message bus. + /// + [PerformanceCounter(Name = "Message Bus Topics Current", Description = "The number of topics in the message bus.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter MessageBusTopicsCurrent { get; private set; } + + /// + /// Gets the performance counter representing the total number of all errors processed since the application was started. + /// + [PerformanceCounter(Name = "Errors: All Total", Description = "The total number of all errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ErrorsAllTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of all errors processed per second. + /// + [PerformanceCounter(Name = "Errors: All/Sec", Description = "The number of all errors processed per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ErrorsAllPerSec { get; private set; } + + /// + /// Gets the performance counter representing the total number of hub resolution errors processed since the application was started. + /// + [PerformanceCounter(Name = "Errors: Hub Resolution Total", Description = "The total number of hub resolution errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ErrorsHubResolutionTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of hub resolution errors per second. + /// + [PerformanceCounter(Name = "Errors: Hub Resolution/Sec", Description = "The number of hub resolution errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ErrorsHubResolutionPerSec { get; private set; } + + /// + /// Gets the performance counter representing the total number of hub invocation errors processed since the application was started. + /// + [PerformanceCounter(Name = "Errors: Hub Invocation Total", Description = "The total number of hub invocation errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ErrorsHubInvocationTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of hub invocation errors per second. + /// + [PerformanceCounter(Name = "Errors: Hub Invocation/Sec", Description = "The number of hub invocation errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ErrorsHubInvocationPerSec { get; private set; } + + /// + /// Gets the performance counter representing the total number of transport errors processed since the application was started. + /// + [PerformanceCounter(Name = "Errors: Tranport Total", Description = "The total number of transport errors processed since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ErrorsTransportTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of transport errors per second. + /// + [PerformanceCounter(Name = "Errors: Transport/Sec", Description = "The number of transport errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ErrorsTransportPerSec { get; private set; } + + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider. + /// + [PerformanceCounter(Name = "Scaleout Streams Total", Description = "The number of logical streams in the currently configured scaleout message bus provider.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ScaleoutStreamCountTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the open state. + /// + [PerformanceCounter(Name = "Scaleout Streams Open", Description = "The number of logical streams in the currently configured scaleout message bus provider that are in the open state", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ScaleoutStreamCountOpen { get; private set; } + + /// + /// Gets the performance counter representing the number of logical streams in the currently configured scaleout message bus provider that are in the buffering state. + /// + [PerformanceCounter(Name = "Scaleout Streams Buffering", Description = "The number of logical streams in the currently configured scaleout message bus provider that are in the buffering state", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ScaleoutStreamCountBuffering { get; private set; } + + /// + /// Gets the performance counter representing the total number of scaleout errors since the application was started. + /// + [PerformanceCounter(Name = "Scaleout Errors Total", Description = "The total number of scaleout errors since the application was started.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ScaleoutErrorsTotal { get; private set; } + + /// + /// Gets the performance counter representing the number of scaleout errors per second. + /// + [PerformanceCounter(Name = "Scaleout Errors/Sec", Description = "The number of scaleout errors per second.", CounterType = PerformanceCounterType.RateOfCountsPerSecond32)] + public IPerformanceCounter ScaleoutErrorsPerSec { get; private set; } + + /// + /// Gets the performance counter representing the current scaleout send queue length. + /// + [PerformanceCounter(Name = "Scaleout Send Queue Length", Description = "The current scaleout send queue length.", CounterType = PerformanceCounterType.NumberOfItems32)] + public IPerformanceCounter ScaleoutSendQueueLength { get; private set; } + + /// + /// Initializes the performance counters. + /// + /// The host instance name. + /// The CancellationToken representing the host shutdown. + public void Initialize(string instanceName, CancellationToken hostShutdownToken) + { + if (_initialized) + { + return; + } + + var needToRegisterWithShutdownToken = false; + lock (_initLocker) + { + if (!_initialized) + { + instanceName = instanceName ?? Guid.NewGuid().ToString(); + SetCounterProperties(instanceName); + // The initializer ran, so let's register the shutdown cleanup + if (hostShutdownToken != CancellationToken.None) + { + needToRegisterWithShutdownToken = true; + } + _initialized = true; + } + } + + if (needToRegisterWithShutdownToken) + { + hostShutdownToken.Register(UnloadCounters); + } + } + + private void UnloadCounters() + { + lock (_initLocker) + { + if (!_initialized) + { + // We were never initalized + return; + } + } + + var counterProperties = this.GetType() + .GetProperties() + .Where(p => p.PropertyType == typeof(IPerformanceCounter)); + + foreach (var property in counterProperties) + { + var counter = property.GetValue(this, null) as IPerformanceCounter; + counter.Close(); + counter.RemoveInstance(); + } + } + + private void InitNoOpCounters() + { + // Set all the counter properties to no-op by default. + // These will get reset to real counters when/if the Initialize method is called. + foreach (var property in _counterProperties) + { + property.SetValue(this, new NoOpPerformanceCounter(), null); + } + } + + private void SetCounterProperties(string instanceName) + { + var loadCounters = true; + + foreach (var property in _counterProperties) + { + PerformanceCounterAttribute attribute = GetPerformanceCounterAttribute(property); + + if (attribute == null) + { + continue; + } + + IPerformanceCounter counter = null; + + if (loadCounters) + { + counter = LoadCounter(CategoryName, attribute.Name, instanceName, isReadOnly:false); + + if (counter == null) + { + // We failed to load the counter so skip the rest + loadCounters = false; + } + } + + counter = counter ?? _noOpCounter; + + property.SetValue(this, counter, null); + } + } + + internal static PropertyInfo[] GetCounterPropertyInfo() + { + return typeof(PerformanceCounterManager) + .GetProperties() + .Where(p => p.PropertyType == typeof(IPerformanceCounter)) + .ToArray(); + } + + internal static PerformanceCounterAttribute GetPerformanceCounterAttribute(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(PerformanceCounterAttribute), false) + .Cast() + .SingleOrDefault(); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This file is shared")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Counters are disposed later")] + public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly) + { + // See http://msdn.microsoft.com/en-us/library/356cx381.aspx for the list of exceptions + // and when they are thrown. + try + { + var counter = new PerformanceCounter(categoryName, counterName, instanceName, isReadOnly); + + // Initialize the counter sample + counter.NextSample(); + + return new PerformanceCounterWrapper(counter); + } +#if UTILS + catch (InvalidOperationException) { return null; } + catch (UnauthorizedAccessException) { return null; } + catch (Win32Exception) { return null; } + catch (PlatformNotSupportedException) { return null; } +#else + catch (InvalidOperationException ex) + { + _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); + return null; + } + catch (UnauthorizedAccessException ex) + { + _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); + return null; + } + catch (Win32Exception ex) + { + _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); + return null; + } + catch (PlatformNotSupportedException ex) + { + _trace.TraceEvent(TraceEventType.Error, 0, "Performance counter failed to load: " + ex.GetBaseException()); + return null; + } +#endif + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs new file mode 100644 index 000000000..8b5a4d6fa --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PerformanceCounterWrapper.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class PerformanceCounterWrapper : IPerformanceCounter + { + private readonly PerformanceCounter _counter; + + public PerformanceCounterWrapper(PerformanceCounter counter) + { + _counter = counter; + } + + public string CounterName + { + get + { + return _counter.CounterName; + } + } + + public long RawValue + { + get { return _counter.RawValue; } + set { _counter.RawValue = value; } + } + + public long Decrement() + { + return _counter.Decrement(); + } + + public long Increment() + { + return _counter.Increment(); + } + + public long IncrementBy(long value) + { + return _counter.IncrementBy(value); + } + + public void Close() + { + _counter.Close(); + } + + public void RemoveInstance() + { + try + { + _counter.RemoveInstance(); + } + catch(NotImplementedException) + { + // This happens on mono + } + } + + public CounterSample NextSample() + { + return _counter.NextSample(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs new file mode 100644 index 000000000..b27334591 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PersistentConnectionContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class PersistentConnectionContext : IPersistentConnectionContext + { + public PersistentConnectionContext(IConnection connection, IConnectionGroupManager groupManager) + { + Connection = connection; + Groups = groupManager; + } + + public IConnection Connection { get; private set; } + + public IConnectionGroupManager Groups { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs new file mode 100644 index 000000000..b734952e5 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/PrefixHelper.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal static class PrefixHelper + { + // Hubs + internal const string HubPrefix = "h-"; + internal const string HubGroupPrefix = "hg-"; + internal const string HubConnectionIdPrefix = "hc-"; + + // Persistent Connections + internal const string PersistentConnectionPrefix = "pc-"; + internal const string PersistentConnectionGroupPrefix = "pcg-"; + + // Both + internal const string ConnectionIdPrefix = "c-"; + internal const string AckPrefix = "ack-"; + + public static bool HasGroupPrefix(string value) + { + return value.StartsWith(HubGroupPrefix, StringComparison.Ordinal) || + value.StartsWith(PersistentConnectionGroupPrefix, StringComparison.Ordinal); + } + + public static string GetConnectionId(string connectionId) + { + return ConnectionIdPrefix + connectionId; + } + + public static string GetHubConnectionId(string connectionId) + { + return HubConnectionIdPrefix + connectionId; + } + + public static string GetHubName(string connectionId) + { + return HubPrefix + connectionId; + } + + public static string GetHubGroupName(string groupName) + { + return HubGroupPrefix + groupName; + } + + public static string GetPersistentConnectionGroupName(string groupName) + { + return PersistentConnectionGroupPrefix + groupName; + } + + public static string GetPersistentConnectionName(string connectionName) + { + return PersistentConnectionPrefix + connectionName; + } + + public static string GetAck(string connectionId) + { + return AckPrefix + connectionId; + } + + public static IList GetPrefixedConnectionIds(IList connectionIds) + { + if (connectionIds.Count == 0) + { + return ListHelper.Empty; + } + + return connectionIds.Select(PrefixHelper.GetConnectionId).ToList(); + } + + public static IEnumerable RemoveGroupPrefixes(IEnumerable groups) + { + return groups.Select(PrefixHelper.RemoveGroupPrefix); + } + + public static string RemoveGroupPrefix(string name) + { + if (name.StartsWith(HubGroupPrefix, StringComparison.Ordinal)) + { + return name.Substring(HubGroupPrefix.Length); + } + + if (name.StartsWith(PersistentConnectionGroupPrefix, StringComparison.Ordinal)) + { + return name.Substring(PersistentConnectionGroupPrefix.Length); + } + + return name; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs new file mode 100644 index 000000000..c2d87415e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Purposes.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + // These need to change when the format changes + public static class Purposes + { + public const string ConnectionToken = "SignalR.ConnectionToken"; + public const string Groups = "SignalR.Groups.v1.1"; + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs new file mode 100644 index 000000000..fb45915ff --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeCancellationTokenSource.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Thread safe cancellation token source. Allows the following: + /// - Cancel will no-op if the token is disposed. + /// - Dispose may be called after Cancel. + /// + internal class SafeCancellationTokenSource : IDisposable + { + private CancellationTokenSource _cts; + private int _state; + + public SafeCancellationTokenSource() + { + _cts = new CancellationTokenSource(); + Token = _cts.Token; + } + + public CancellationToken Token { get; private set; } + + public void Cancel() + { + var value = Interlocked.CompareExchange(ref _state, State.Cancelling, State.Initial); + + if (value == State.Initial) + { + // Because cancellation tokens are so poorly behaved, always invoke the cancellation token on + // another thread. Don't capture any of the context (execution context or sync context) + // while doing this. +#if WINDOWS_PHONE || SILVERLIGHT + ThreadPool.QueueUserWorkItem(_ => +#elif NETFX_CORE + Task.Run(() => +#else + ThreadPool.UnsafeQueueUserWorkItem(_ => +#endif + { + try + { + _cts.Cancel(); + } + finally + { + if (Interlocked.CompareExchange(ref _state, State.Cancelled, State.Cancelling) == State.Disposing) + { + _cts.Dispose(); + Interlocked.Exchange(ref _state, State.Disposed); + } + } + } +#if !NETFX_CORE + , state: null +#endif +); + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + var value = Interlocked.Exchange(ref _state, State.Disposing); + + switch (value) + { + case State.Initial: + case State.Cancelled: + _cts.Dispose(); + Interlocked.Exchange(ref _state, State.Disposed); + break; + case State.Cancelling: + case State.Disposing: + // No-op + break; + case State.Disposed: + Interlocked.Exchange(ref _state, State.Disposed); + break; + default: + break; + } + } + } + + public void Dispose() + { + Dispose(true); + } + + private static class State + { + public const int Initial = 0; + public const int Cancelling = 1; + public const int Cancelled = 2; + public const int Disposing = 3; + public const int Disposed = 4; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs new file mode 100644 index 000000000..ae9827a27 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SafeSet.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class SafeSet + { + private readonly ConcurrentDictionary _items; + + public SafeSet() + { + _items = new ConcurrentDictionary(); + } + + public SafeSet(IEqualityComparer comparer) + { + _items = new ConcurrentDictionary(comparer); + } + + public SafeSet(IEnumerable items) + { + _items = new ConcurrentDictionary(items.Select(x => new KeyValuePair(x, null))); + } + + public ICollection GetSnapshot() + { + // The Keys property locks, so Select instead + return _items.Keys; + } + + public bool Contains(T item) + { + return _items.ContainsKey(item); + } + + public bool Add(T item) + { + return _items.TryAdd(item, null); + } + + public bool Remove(T item) + { + object _; + return _items.TryRemove(item, out _); + } + + public bool Any() + { + return _items.Any(); + } + + public long Count + { + get { return _items.Count; } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs new file mode 100644 index 000000000..604144187 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommand.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// A server to server command. + /// + internal class ServerCommand + { + /// + /// Gets or sets the id of the command where this message originated from. + /// + public string ServerId { get; set; } + + /// + /// Gets of sets the command type. + /// + public ServerCommandType ServerCommandType { get; set; } + + /// + /// Gets or sets the value for this command. + /// + public object Value { get; set; } + + internal bool IsFromSelf(string serverId) + { + return serverId.Equals(ServerId); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs new file mode 100644 index 000000000..aecdfa28a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandHandler.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Default implementation. + /// + internal class ServerCommandHandler : IServerCommandHandler, ISubscriber, IDisposable + { + private readonly IMessageBus _messageBus; + private readonly IServerIdManager _serverIdManager; + private readonly IJsonSerializer _serializer; + private IDisposable _subscription; + + private const int MaxMessages = 10; + + // The signal for all signalr servers + private const string ServerSignal = "__SIGNALR__SERVER__"; + private static readonly string[] ServerSignals = new[] { ServerSignal }; + + public ServerCommandHandler(IDependencyResolver resolver) : + this(resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve()) + { + + } + + public ServerCommandHandler(IMessageBus messageBus, IServerIdManager serverIdManager, IJsonSerializer serializer) + { + _messageBus = messageBus; + _serverIdManager = serverIdManager; + _serializer = serializer; + + ProcessMessages(); + } + + public Action Command + { + get; + set; + } + + + public IList EventKeys + { + get + { + return ServerSignals; + } + } + + event Action ISubscriber.EventKeyAdded + { + add + { + } + remove + { + } + } + + event Action ISubscriber.EventKeyRemoved + { + add + { + } + remove + { + } + } + + public Action WriteCursor { get; set; } + + public string Identity + { + get + { + return _serverIdManager.ServerId; + } + } + + public Subscription Subscription + { + get; + set; + } + + public Task SendCommand(ServerCommand command) + { + // Store where the message originated from + command.ServerId = _serverIdManager.ServerId; + + // Send the command to the all servers + return _messageBus.Publish(_serverIdManager.ServerId, ServerSignal, _serializer.Stringify(command)); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_subscription != null) + { + _subscription.Dispose(); + } + } + } + + public void Dispose() + { + Dispose(true); + } + + private void ProcessMessages() + { + // Process messages that come from the bus for servers + _subscription = _messageBus.Subscribe(this, cursor: null, callback: HandleServerCommands, maxMessages: MaxMessages, state: null); + } + + private Task HandleServerCommands(MessageResult result, object state) + { + result.Messages.Enumerate(m => ServerSignal.Equals(m.Key), + (s, m) => + { + var command = _serializer.Parse(m.Value, m.Encoding); + OnCommand(command); + }, + state: null); + + return TaskAsyncHelper.True; + } + + private void OnCommand(ServerCommand command) + { + if (Command != null) + { + Command(command); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs new file mode 100644 index 000000000..058164117 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerCommandType.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + public enum ServerCommandType + { + RemoveConnection + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs new file mode 100644 index 000000000..e80d5cae1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ServerIdManager.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + /// + /// Default implementation. + /// + public class ServerIdManager : IServerIdManager + { + public ServerIdManager() + { + ServerId = Guid.NewGuid().ToString(); + } + + /// + /// The id of the server. + /// + public string ServerId + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs new file mode 100644 index 000000000..bf86a93f3 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/SipHashBasedStringEqualityComparer.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + // A string equality comparer based on the SipHash-2-4 algorithm. Key differences: + // (a) we output 32-bit hashes instead of 64-bit hashes, and + // (b) we don't care about endianness since hashes are used only in hash tables + // and aren't returned to user code. + // + // Meant to serve as a replacement for StringComparer.Ordinal. + // Derivative work of https://github.com/tanglebones/ch-siphash. + internal unsafe sealed class SipHashBasedStringEqualityComparer : IEqualityComparer + { + private static readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + + // the 128-bit secret key + private readonly ulong _k0; + private readonly ulong _k1; + + public SipHashBasedStringEqualityComparer() + : this(GenerateRandomKeySegment(), GenerateRandomKeySegment()) + { + } + + // for unit testing + internal SipHashBasedStringEqualityComparer(ulong k0, ulong k1) + { + _k0 = k0; + _k1 = k1; + } + + public bool Equals(string x, string y) + { + return String.Equals(x, y); + } + + private static ulong GenerateRandomKeySegment() + { + byte[] bytes = new byte[sizeof(ulong)]; + _rng.GetBytes(bytes); + return (ulong)BitConverter.ToInt64(bytes, 0); + } + + public int GetHashCode(string obj) + { + if (obj == null) + { + return 0; + } + + fixed (char* pChars = obj) + { + // treat input as an opaque blob, convert char count to byte count + return GetHashCode((byte*)pChars, checked((uint)obj.Length * sizeof(char))); + } + } + + // for unit testing + internal int GetHashCode(byte* bytes, uint len) + { + // Assume SipHash-2-4 is a strong PRF, therefore truncation to 32 bits is acceptable. + return (int)SipHash_2_4_UlongCast_ForcedInline(bytes, len, _k0, _k1); + } + + private static unsafe ulong SipHash_2_4_UlongCast_ForcedInline(byte* finb, uint inlen, ulong k0, ulong k1) + { + var v0 = 0x736f6d6570736575 ^ k0; + var v1 = 0x646f72616e646f6d ^ k1; + var v2 = 0x6c7967656e657261 ^ k0; + var v3 = 0x7465646279746573 ^ k1; + + var b = ((ulong)inlen) << 56; + + if (inlen > 0) + { + var inb = finb; + var left = inlen & 7; + var end = inb + inlen - left; + var linb = (ulong*)finb; + var lend = (ulong*)end; + for (; linb < lend; ++linb) + { + v3 ^= *linb; + + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + + v0 ^= *linb; + } + for (var i = 0; i < left; ++i) + { + b |= ((ulong)end[i]) << (8 * i); + } + } + + v3 ^= b; + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 ^= b; + v2 ^= 0xff; + + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + v0 += v1; + v1 = (v1 << 13) | (v1 >> (64 - 13)); + v1 ^= v0; + v0 = (v0 << 32) | (v0 >> (64 - 32)); + + v2 += v3; + v3 = (v3 << 16) | (v3 >> (64 - 16)); + v3 ^= v2; + + v0 += v3; + v3 = (v3 << 21) | (v3 >> (64 - 21)); + v3 ^= v0; + + v2 += v1; + v1 = (v1 << 17) | (v1 >> (64 - 17)); + v1 ^= v2; + v2 = (v2 << 32) | (v2 >> (64 - 32)); + + return v0 ^ v1 ^ v2 ^ v3; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs new file mode 100644 index 000000000..ecbb249ef --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/StringMinifier.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + internal class StringMinifier : IStringMinifier + { + private readonly ConcurrentDictionary _stringMinifier = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _stringMaximizer = new ConcurrentDictionary(); + private int _lastMinifiedKey = -1; + + private readonly Func _createMinifiedString; + + public StringMinifier() + { + _createMinifiedString = CreateMinifiedString; + } + + public string Minify(string fullString) + { + return _stringMinifier.GetOrAdd(fullString, _createMinifiedString); + } + + public string Unminify(string minifiedString) + { + string result; + _stringMaximizer.TryGetValue(minifiedString, out result); + return result; + } + + public void RemoveUnminified(string fullString) + { + string minifiedString; + if (_stringMinifier.TryRemove(fullString, out minifiedString)) + { + string value; + _stringMaximizer.TryRemove(minifiedString, out value); + } + } + + private string CreateMinifiedString(string fullString) + { + var minString = GetStringFromInt((uint)Interlocked.Increment(ref _lastMinifiedKey)); + _stringMaximizer.TryAdd(minString, fullString); + return minString; + } + + [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This is a valid exception to throw.")] + private static char GetCharFromSixBitInt(uint num) + { + if (num < 26) + { + return (char)(num + 'A'); + } + if (num < 52) + { + return (char)(num - 26 + 'a'); + } + if (num < 62) + { + return (char)(num - 52 + '0'); + } + if (num == 62) + { + return '_'; + } + if (num == 63) + { + return ':'; + } + throw new IndexOutOfRangeException(); + } + + private static string GetStringFromInt(uint num) + { + const int maxSize = 6; + + // Buffer must be large enough to store any 32 bit uint at 6 bits per character + var buffer = new char[maxSize]; + var index = maxSize; + do + { + // Append next 6 bits of num + buffer[--index] = GetCharFromSixBitInt(num & 0x3f); + num >>= 6; + + // Don't pad output string, but ensure at least one character is written + } while (num != 0); + + return new string(buffer, index, maxSize - index); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs new file mode 100644 index 000000000..f12743b75 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/TaskQueue.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + // Allows serial queuing of Task instances + // The tasks are not called on the current synchronization context + + internal sealed class TaskQueue + { + private readonly object _lockObj = new object(); + private Task _lastQueuedTask; + private volatile bool _drained; + private readonly int? _maxSize; + private long _size; + + public TaskQueue() + : this(TaskAsyncHelper.Empty) + { + } + + public TaskQueue(Task initialTask) + { + _lastQueuedTask = initialTask; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] + public TaskQueue(Task initialTask, int maxSize) + { + _lastQueuedTask = initialTask; + _maxSize = maxSize; + } + +#if !CLIENT_NET45 + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code.")] + public IPerformanceCounter QueueSizeCounter { get; set; } +#endif + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] + public bool IsDrained + { + get + { + return _drained; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] + public Task Enqueue(Func taskFunc, object state) + { + // Lock the object for as short amount of time as possible + lock (_lockObj) + { + if (_drained) + { + return _lastQueuedTask; + } + + if (_maxSize != null) + { + if (Interlocked.Read(ref _size) == _maxSize) + { + // REVIEW: Do we need to make the contract more clear between the + // queue full case and the queue drained case? Should we throw an exeception instead? + + // We failed to enqueue because the size limit was reached + return null; + } + + // Increment the size if the queue + Interlocked.Increment(ref _size); + +#if !CLIENT_NET45 + var counter = QueueSizeCounter; + if (counter != null) + { + counter.Increment(); + } +#endif + } + + Task newTask = _lastQueuedTask.Then((next, nextState) => + { + return next(nextState).Finally(s => + { + var queue = (TaskQueue)s; + if (queue._maxSize != null) + { + // Decrement the number of items left in the queue + Interlocked.Decrement(ref queue._size); + +#if !CLIENT_NET45 + var counter = QueueSizeCounter; + if (counter != null) + { + counter.Decrement(); + } +#endif + } + }, + this); + }, + taskFunc, state); + + _lastQueuedTask = newTask; + return newTask; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] + public Task Enqueue(Func taskFunc) + { + return Enqueue(state => ((Func)state).Invoke(), taskFunc); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is shared code")] + public Task Drain() + { + lock (_lockObj) + { + _drained = true; + + return _lastQueuedTask; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs new file mode 100644 index 000000000..d2ee8705d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonSerializer.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Used to serialize and deserialize outgoing/incoming data. + /// + public interface IJsonSerializer + { + /// + /// Serializes the specified object to a . + /// + /// The object to serialize + /// The to serialize the object to. + void Serialize(object value, TextWriter writer); + + /// + /// Deserializes the JSON to a .NET object. + /// + /// The to deserialize the object from. + /// The of object being deserialized. + /// The deserialized object from the JSON string. + object Parse(TextReader reader, Type targetType); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs new file mode 100644 index 000000000..4e462abb7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonValue.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Represents a JSON value. + /// + public interface IJsonValue + { + /// + /// Converts the parameter value to the specified . + /// + /// The to convert the parameter to. + /// The converted parameter value. + object ConvertTo(Type type); + + /// + /// Determines if the parameter can be converted to the specified . + /// + /// The to check. + /// True if the parameter can be converted to the specified , false otherwise. + bool CanConvertTo(Type type); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs new file mode 100644 index 000000000..d1b73ab37 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/IJsonWritable.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.IO; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Implementations handle their own serialization to JSON. + /// + public interface IJsonWritable + { + /// + /// Serializes itself to JSON via a . + /// + /// The that receives the JSON serialized object. + void WriteJson(TextWriter writer); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs new file mode 100644 index 000000000..df22e7078 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/JRawValue.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// An implementation of IJsonValue over JSON.NET + /// + internal class JRawValue : IJsonValue + { + private readonly string _value; + + public JRawValue(JRaw value) + { + _value = value.ToString(); + } + + public object ConvertTo(Type type) + { + // A non generic implementation of ToObject on JToken + using (var jsonReader = new StringReader(_value)) + { + var settings = new JsonSerializerSettings + { + MaxDepth = 20 + }; + var serializer = JsonSerializer.Create(settings); + return serializer.Deserialize(jsonReader, type); + } + } + + public bool CanConvertTo(Type type) + { + // TODO: Implement when we implement better method overload resolution + return true; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs new file mode 100644 index 000000000..39d72e472 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/JsonNetSerializer.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Default implementation over Json.NET. + /// + public class JsonNetSerializer : IJsonSerializer + { + private readonly JsonSerializer _serializer; + + /// + /// Initializes a new instance of the class. + /// + public JsonNetSerializer() + : this(new JsonSerializerSettings()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use when serializing and deserializing. + public JsonNetSerializer(JsonSerializerSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException("settings"); + } + + // Just override it anyways (we're saving the user) + settings.MaxDepth = 20; + _serializer = JsonSerializer.Create(settings); + } + + /// + /// Deserializes the JSON to a .NET object. + /// + /// The JSON to deserialize. + /// The of object being deserialized. + /// The deserialized object from the JSON string. + public object Parse(TextReader reader, Type targetType) + { + return _serializer.Deserialize(reader, targetType); + } + + /// + /// Serializes the specified object to a . + /// + /// The object to serialize + /// The to serialize the object to. + public void Serialize(object value, TextWriter writer) + { + var selfSerializer = value as IJsonWritable; + if (selfSerializer != null) + { + selfSerializer.WriteJson(writer); + } + else + { + _serializer.Serialize(writer, value); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs new file mode 100644 index 000000000..2a34ad264 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/JsonSerializerExtensions.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Extensions for . + /// + public static class JsonSerializerExtensions + { + /// + /// Deserializes the JSON to a .NET object. + /// + /// The serializer + /// The of object being deserialized. + /// The JSON to deserialize + /// The deserialized object from the JSON string. + public static T Parse(this IJsonSerializer serializer, string json) + { + if (serializer == null) + { + throw new ArgumentNullException("serializer"); + } + + using (var reader = new StringReader(json)) + { + return (T)serializer.Parse(reader, typeof(T)); + } + } + + /// + /// Deserializes the JSON to a .NET object. + /// + /// The serializer + /// The of object being deserialized. + /// The JSON buffer to deserialize + /// The encoding to use. + /// The deserialized object from the JSON string. + public static T Parse(this IJsonSerializer serializer, ArraySegment jsonBuffer, Encoding encoding) + { + if (serializer == null) + { + throw new ArgumentNullException("serializer"); + } + + using (var reader = new ArraySegmentTextReader(jsonBuffer, encoding)) + { + return (T)serializer.Parse(reader, typeof(T)); + } + } + + /// + /// Serializes the specified object to a JSON string. + /// + /// The serializer + /// The object to serailize. + /// A JSON string representation of the object. + public static string Stringify(this IJsonSerializer serializer, object value) + { + if (serializer == null) + { + throw new ArgumentNullException("serializer"); + } + + using (var writer = new StringWriter(CultureInfo.InvariantCulture)) + { + serializer.Serialize(value, writer); + return writer.ToString(); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs b/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs new file mode 100644 index 000000000..4acec1024 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/JsonUtility.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.CodeDom.Compiler; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// Helper class for common JSON operations. + /// + public static class JsonUtility + { + // JavaScript keywords taken from http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf + // Sections: 7.6.1.1, 7.6.1.2 + // Plus the implicity globals "NaN", "undefined", "Infinity" + private static readonly string[] _jsKeywords = new[] { "break", "do", "instanceof", "typeof", "case", "else", "new", "var", "catch", "finally", "return", "void", "continue", "for", "switch", "while", "debugger", "function", "this", "with", "default", "if", "throw", "delete", "in", "try", "class", "enum", "extends", "super", "const", "export", "import", "implements", "let", "private", "public", "yield", "interface", "package", "protected", "static", "NaN", "undefined", "Infinity" }; + + /// + /// Converts the specified name to camel case. + /// + /// The name to convert. + /// A camel cased version of the specified name. + public static string CamelCase(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + return String.Join(".", name.Split('.').Select(n => Char.ToLower(n[0], CultureInfo.InvariantCulture) + n.Substring(1))); + } + + /// + /// Gets a string that returns JSON mime type "application/json; charset=UTF-8". + /// + public static string JsonMimeType + { + get { return "application/json; charset=UTF-8"; } + } + + /// + /// Gets a string that returns JSONP mime type "application/javascript; charset=UTF-8". + /// + public static string JavaScriptMimeType + { + get { return "application/javascript; charset=UTF-8"; } + } + + public static string CreateJsonpCallback(string callback, string payload) + { + var sb = new StringBuilder(); + if (!IsValidJavaScriptCallback(callback)) + { + throw new InvalidOperationException(); + } + sb.AppendFormat("{0}(", callback).Append(payload).Append(");"); + return sb.ToString(); + } + + internal static bool IsValidJavaScriptCallback(string callback) + { + if (String.IsNullOrWhiteSpace(callback)) + { + return false; + } + + var identifiers = callback.Split('.'); + + // Check each identifier to ensure it's a valid JS identifier + foreach (var identifier in identifiers) + { + if (!IsValidJavaScriptFunctionName(identifier)) + { + return false; + } + } + + return true; + } + + internal static bool IsValidJavaScriptFunctionName(string name) + { + if (String.IsNullOrWhiteSpace(name) || IsJavaScriptReservedWord(name)) + { + return false; + } + + // JavaScript identifier must start with a letter or a '$' or an '_' char + var firstChar = name[0]; + if (!IsValidJavaScriptIdentifierStartChar(firstChar)) + { + return false; + } + + for (var i = 1; i < name.Length; i++) + { + // Characters can be a letter, digit, '$' or '_' + if (!IsValidJavaScriptIdenfitierNonStartChar(name[i])) + { + return false; + } + } + + return true; + } + + private static bool IsValidJavaScriptIdentifierStartChar(char startChar) + { + return Char.IsLetter(startChar) || startChar == '$' || startChar == '_'; + } + + private static bool IsValidJavaScriptIdenfitierNonStartChar(char identifierChar) + { + return Char.IsLetterOrDigit(identifierChar) || identifierChar == '$' || identifierChar == '_'; + } + + private static bool IsJavaScriptReservedWord(string word) + { + return _jsKeywords.Contains(word); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs b/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs new file mode 100644 index 000000000..418d53cf2 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Json/SipHashBasedDictionaryConverter.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.SignalR.Infrastructure; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Json +{ + /// + /// A converter for dictionaries that uses a SipHash comparer + /// + internal class SipHashBasedDictionaryConverter : JsonConverter + { + public override bool CanWrite + { + get + { + return false; + } + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IDictionary); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ReadJsonObject(reader); + } + + private object ReadJsonObject(JsonReader reader) + { + switch (reader.TokenType) + { + case JsonToken.StartObject: + return ReadObject(reader); + case JsonToken.StartArray: + return ReadArray(reader); + case JsonToken.Integer: + case JsonToken.Float: + case JsonToken.String: + case JsonToken.Boolean: + case JsonToken.Undefined: + case JsonToken.Null: + case JsonToken.Date: + case JsonToken.Bytes: + return reader.Value; + default: + throw new NotSupportedException(); + + } + } + + private object ReadArray(JsonReader reader) + { + var array = new List(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + default: + object value = ReadJsonObject(reader); + + array.Add(value); + break; + case JsonToken.EndArray: + return array; + } + } + + throw new JsonSerializationException(Resources.Error_ParseObjectFailed); + } + + private object ReadObject(JsonReader reader) + { + var obj = new Dictionary(new SipHashBasedStringEqualityComparer()); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + string propertyName = reader.Value.ToString(); + + if (!reader.Read()) + { + throw new JsonSerializationException(Resources.Error_ParseObjectFailed); + } + + object value = ReadJsonObject(reader); + + obj[propertyName] = value; + break; + case JsonToken.EndObject: + return obj; + default: + throw new JsonSerializationException(Resources.Error_ParseObjectFailed); + + } + } + + throw new JsonSerializationException(Resources.Error_ParseObjectFailed); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs new file mode 100644 index 000000000..d3d7d104d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Command.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class Command + { + public Command() + { + Id = Guid.NewGuid().ToString(); + } + + public bool WaitForAck { get; set; } + public string Id { get; private set; } + public CommandType CommandType { get; set; } + public string Value { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs new file mode 100644 index 000000000..12bff330b --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/CommandType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public enum CommandType + { + AddToGroup, + RemoveFromGroup, + Disconnect, + Abort + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs new file mode 100644 index 000000000..8099e9a99 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Cursor.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + internal unsafe class Cursor + { + private static char[] _escapeChars = new[] { '\\', '|', ',' }; + private string _escapedKey; + + public string Key { get; private set; } + public ulong Id { get; set; } + + public static Cursor Clone(Cursor cursor) + { + return new Cursor(cursor.Key, cursor.Id, cursor._escapedKey); + } + + public Cursor(string key, ulong id) + : this(key, id, Escape(key)) + { + } + + public Cursor(string key, ulong id, string minifiedKey) + { + Key = key; + Id = id; + _escapedKey = minifiedKey; + } + + public static void WriteCursors(TextWriter textWriter, IList cursors) + { + for (int i = 0; i < cursors.Count; i++) + { + if (i > 0) + { + textWriter.Write('|'); + } + Cursor cursor = cursors[i]; + textWriter.Write(cursor._escapedKey); + textWriter.Write(','); + WriteUlongAsHexToBuffer(cursor.Id, textWriter); + } + } + + private static void WriteUlongAsHexToBuffer(ulong value, TextWriter textWriter) + { + // This tracks the length of the output and serves as the index for the next character to be written into the pBuffer. + // The length could reach up to 16 characters, so at least that much space should remain in the pBuffer. + int length = 0; + + // Write the hex value from left to right into the buffer without zero padding. + for (int i = 0; i < 16; i++) + { + // Convert the first 4 bits of the value to a valid hex character. + char hexChar = Int32ToHex((int)(value >> 60)); + value <<= 4; + + // Don't increment length if it would just add zero padding + if (length != 0 || hexChar != '0') + { + textWriter.Write(hexChar); + length++; + } + } + + if (length == 0) + { + textWriter.Write('0'); + } + } + + private static char Int32ToHex(int value) + { + return (value < 10) ? (char)(value + '0') : (char)(value - 10 + 'A'); + } + + private static string Escape(string value) + { + // Nothing to do, so bail + if (value.IndexOfAny(_escapeChars) == -1) + { + return value; + } + + var sb = new StringBuilder(); + // \\ = \ + // \| = | + // \, = , + foreach (var ch in value) + { + switch (ch) + { + case '\\': + sb.Append('\\').Append(ch); + break; + case '|': + sb.Append('\\').Append(ch); + break; + case ',': + sb.Append('\\').Append(ch); + break; + default: + sb.Append(ch); + break; + } + } + + return sb.ToString(); + } + + public static List GetCursors(string cursor) + { + return GetCursors(cursor, s => s); + } + + public static List GetCursors(string cursor, Func keyMaximizer) + { + return GetCursors(cursor, (key, state) => ((Func)state).Invoke(key), keyMaximizer); + } + + public static List GetCursors(string cursor, Func keyMaximizer, object state) + { + // Technically GetCursors should never be called with a null value, so this is extra cautious + if (String.IsNullOrEmpty(cursor)) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + var signals = new HashSet(); + var cursors = new List(); + string currentKey = null; + string currentEscapedKey = null; + ulong currentId; + bool escape = false; + bool consumingKey = true; + var sb = new StringBuilder(); + var sbEscaped = new StringBuilder(); + Cursor parsedCursor; + + foreach (var ch in cursor) + { + // escape can only be true if we are consuming the key + if (escape) + { + if (ch != '\\' && ch != ',' && ch != '|') + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + sb.Append(ch); + sbEscaped.Append(ch); + escape = false; + } + else + { + if (ch == '\\') + { + if (!consumingKey) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + sbEscaped.Append('\\'); + escape = true; + } + else if (ch == ',') + { + if (!consumingKey) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + // For now String.Empty is an acceptable key, but this should change once we verify + // that empty keys cannot be created legitimately. + currentKey = keyMaximizer(sb.ToString(), state); + + // If the keyMap cannot find a key, we cannot create an array of cursors. + // This most likely means there was an AppDomain restart or a misbehaving client. + if (currentKey == null) + { + return null; + } + // Don't allow duplicate keys + if (!signals.Add(currentKey)) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + currentEscapedKey = sbEscaped.ToString(); + + sb.Clear(); + sbEscaped.Clear(); + consumingKey = false; + } + else if (ch == '|') + { + if (consumingKey) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + ParseCursorId(sb, out currentId); + + parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey); + + cursors.Add(parsedCursor); + sb.Clear(); + consumingKey = true; + } + else + { + sb.Append(ch); + if (consumingKey) + { + sbEscaped.Append(ch); + } + } + } + } + + if (consumingKey) + { + throw new FormatException(Resources.Error_InvalidCursorFormat); + } + + ParseCursorId(sb, out currentId); + + parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey); + + cursors.Add(parsedCursor); + + return cursors; + } + + private static void ParseCursorId(StringBuilder sb, out ulong id) + { + string value = sb.ToString(); + id = UInt64.Parse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + public override string ToString() + { + return Key; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs new file mode 100644 index 000000000..17c3e0e2d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/DefaultSubscription.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + internal class DefaultSubscription : Subscription + { + private List _cursors; + private List _cursorTopics; + + private readonly IStringMinifier _stringMinifier; + + public DefaultSubscription(string identity, + IList eventKeys, + TopicLookup topics, + string cursor, + Func> callback, + int maxMessages, + IStringMinifier stringMinifier, + IPerformanceCounterManager counters, + object state) : + base(identity, eventKeys, callback, maxMessages, counters, state) + { + _stringMinifier = stringMinifier; + + if (String.IsNullOrEmpty(cursor)) + { + _cursors = GetCursorsFromEventKeys(EventKeys, topics); + } + else + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + _cursors = Cursor.GetCursors(cursor, (k, s) => UnminifyCursor(k, s), stringMinifier) ?? GetCursorsFromEventKeys(EventKeys, topics); + } + + _cursorTopics = new List(); + + if (!String.IsNullOrEmpty(cursor)) + { + // Update all of the cursors so we're within the range + for (int i = _cursors.Count - 1; i >= 0; i--) + { + Cursor c = _cursors[i]; + Topic topic; + if (!EventKeys.Contains(c.Key)) + { + _cursors.Remove(c); + } + else if (!topics.TryGetValue(_cursors[i].Key, out topic) || _cursors[i].Id > topic.Store.GetMessageCount()) + { + UpdateCursor(c.Key, 0); + } + } + } + + // Add dummy entries so they can be filled in + for (int i = 0; i < _cursors.Count; i++) + { + _cursorTopics.Add(null); + } + } + + private static string UnminifyCursor(string key, object state) + { + return ((IStringMinifier)state).Unminify(key); + } + + public override bool AddEvent(string eventKey, Topic topic) + { + base.AddEvent(eventKey, topic); + + lock (_cursors) + { + // O(n), but small n and it's not common + var index = _cursors.FindIndex(c => c.Key == eventKey); + if (index == -1) + { + _cursors.Add(new Cursor(eventKey, GetMessageId(topic), _stringMinifier.Minify(eventKey))); + + _cursorTopics.Add(topic); + + return true; + } + + return false; + } + } + + public override void RemoveEvent(string eventKey) + { + base.RemoveEvent(eventKey); + + lock (_cursors) + { + var index = _cursors.FindIndex(c => c.Key == eventKey); + if (index != -1) + { + _cursors.RemoveAt(index); + _cursorTopics.RemoveAt(index); + } + } + } + + public override void SetEventTopic(string eventKey, Topic topic) + { + base.SetEventTopic(eventKey, topic); + + lock (_cursors) + { + // O(n), but small n and it's not common + var index = _cursors.FindIndex(c => c.Key == eventKey); + if (index != -1) + { + _cursorTopics[index] = topic; + } + } + } + + public override void WriteCursor(TextWriter textWriter) + { + lock (_cursors) + { + Cursor.WriteCursors(textWriter, _cursors); + } + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")] + protected override void PerformWork(IList> items, out int totalCount, out object state) + { + totalCount = 0; + + lock (_cursors) + { + var cursors = new ulong[_cursors.Count]; + for (int i = 0; i < _cursors.Count; i++) + { + MessageStoreResult storeResult = _cursorTopics[i].Store.GetMessages(_cursors[i].Id, MaxMessages); + cursors[i] = storeResult.FirstMessageId + (ulong)storeResult.Messages.Count; + + if (storeResult.Messages.Count > 0) + { + items.Add(storeResult.Messages); + totalCount += storeResult.Messages.Count; + } + } + + // Return the state as a list of cursors + state = cursors; + } + } + + protected override void BeforeInvoke(object state) + { + lock (_cursors) + { + // Update the list of cursors before invoking anything + var nextCursors = (ulong[])state; + for (int i = 0; i < _cursors.Count; i++) + { + _cursors[i].Id = nextCursors[i]; + } + } + } + + private bool UpdateCursor(string key, ulong id) + { + lock (_cursors) + { + // O(n), but small n and it's not common + var index = _cursors.FindIndex(c => c.Key == key); + if (index != -1) + { + _cursors[index].Id = id; + return true; + } + + return false; + } + } + + private List GetCursorsFromEventKeys(IList eventKeys, TopicLookup topics) + { + var list = new List(eventKeys.Count); + foreach (var eventKey in eventKeys) + { + var cursor = new Cursor(eventKey, GetMessageId(topics, eventKey), _stringMinifier.Minify(eventKey)); + list.Add(cursor); + } + + return list; + } + + private static ulong GetMessageId(TopicLookup topics, string key) + { + Topic topic; + if (topics.TryGetValue(key, out topic)) + { + return GetMessageId(topic); + } + return 0; + } + + private static ulong GetMessageId(Topic topic) + { + if (topic == null) + { + return 0; + } + + return topic.Store.GetMessageCount(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs new file mode 100644 index 000000000..b52510ca1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/IMessageBus.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public interface IMessageBus + { + /// + /// + /// + /// + /// + Task Publish(Message message); + + /// + /// + /// + /// + /// + /// + /// + /// + /// + IDisposable Subscribe(ISubscriber subscriber, string cursor, Func> callback, int maxMessages, object state); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs new file mode 100644 index 000000000..317ab4d5f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscriber.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public interface ISubscriber + { + IList EventKeys { get; } + + Action WriteCursor { get; set; } + + string Identity { get; } + + event Action EventKeyAdded; + + event Action EventKeyRemoved; + + Subscription Subscription { get; set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs new file mode 100644 index 000000000..fa941ff74 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ISubscription.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public interface ISubscription + { + string Identity { get; } + + bool SetQueued(); + bool UnsetQueued(); + + Task Work(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs new file mode 100644 index 000000000..2232cde05 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/LocalEventKeyInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class LocalEventKeyInfo + { + private readonly WeakReference _storeReference; + + public LocalEventKeyInfo(string key, ulong id, MessageStore store) + { + // Don't hold onto MessageStores that would otherwise be GC'd + _storeReference = new WeakReference(store); + Key = key; + Id = id; + } + + public string Key { get; private set; } + public ulong Id { get; private set; } + public MessageStore MessageStore + { + get + { + return _storeReference.Target as MessageStore; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs new file mode 100644 index 000000000..bb1dbffd0 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Message.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class Message + { + private static readonly byte[] _zeroByteBuffer = new byte[0]; + private static readonly UTF8Encoding _encoding = new UTF8Encoding(); + + public Message() + { + Encoding = _encoding; + } + + public Message(string source, string key, string value) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (key == null) + { + throw new ArgumentNullException("key"); + } + + Source = source; + Key = key; + Encoding = _encoding; + Value = value == null ? new ArraySegment(_zeroByteBuffer) : new ArraySegment(Encoding.GetBytes(value)); + } + + public Message(string source, string key, ArraySegment value) + : this() + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (key == null) + { + throw new ArgumentNullException("key"); + } + + Source = source; + Key = key; + Value = value; + } + + /// + /// Which connection the message originated from + /// + public string Source { get; set; } + + /// + /// The signal for the message (connection id, group, etc) + /// + public string Key { get; set; } + + /// + /// The message payload + /// + public ArraySegment Value { get; set; } + + /// + /// The command id if this message is a command + /// + public string CommandId { get; set; } + + /// + /// Determines if the caller should wait for acknowledgement for this message + /// + public bool WaitForAck { get; set; } + + /// + /// Determines if this message is itself an ACK + /// + public bool IsAck { get; set; } + + /// + /// A list of connection ids to filter out + /// + public string Filter { get; set; } + + /// + /// The encoding of the message + /// + public Encoding Encoding { get; private set; } + + /// + /// The payload id. Only used in scaleout scenarios + /// + public ulong MappingId { get; set; } + + /// + /// The stream index this message came from. Only used the scaleout scenarios. + /// + public int StreamIndex { get; set; } + + public bool IsCommand + { + get + { + return !String.IsNullOrEmpty(CommandId); + } + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This may be expensive")] + public string GetString() + { + // If there's no encoding this is a raw binary payload + if (Encoding == null) + { + throw new NotSupportedException(); + } + + return Encoding.GetString(Value.Array, Value.Offset, Value.Count); + } + + public void WriteTo(Stream stream) + { + var binaryWriter = new BinaryWriter(stream); + binaryWriter.Write(Source); + binaryWriter.Write(Key); + binaryWriter.Write(Value.Count); + binaryWriter.Write(Value.Array, Value.Offset, Value.Count); + binaryWriter.Write(CommandId ?? String.Empty); + binaryWriter.Write(WaitForAck); + binaryWriter.Write(IsAck); + binaryWriter.Write(Filter ?? String.Empty); + } + + public static Message ReadFrom(Stream stream) + { + var message = new Message(); + var binaryReader = new BinaryReader(stream); + message.Source = binaryReader.ReadString(); + message.Key = binaryReader.ReadString(); + int bytes = binaryReader.ReadInt32(); + message.Value = new ArraySegment(binaryReader.ReadBytes(bytes)); + message.CommandId = binaryReader.ReadString(); + message.WaitForAck = binaryReader.ReadBoolean(); + message.IsAck = binaryReader.ReadBoolean(); + message.Filter = binaryReader.ReadString(); + + return message; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs new file mode 100644 index 000000000..76c24161d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBroker.cs @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// This class is the main coordinator. It schedules work to be done for a particular subscription + /// and has an algorithm for choosing a number of workers (thread pool threads), to handle + /// the scheduled work. + /// + public class MessageBroker : IDisposable + { + private readonly Queue _queue = new Queue(); + + private readonly IPerformanceCounterManager _counters; + + // The maximum number of workers (threads) allowed to process all incoming messages + private readonly int _maxWorkers; + + // The maximum number of workers that can be left to idle (not busy but allocated) + private readonly int _maxIdleWorkers; + + // The number of allocated workers (currently running) + private int _allocatedWorkers; + + // The number of workers that are *actually* doing work + private int _busyWorkers; + + // Determines if the broker was disposed and should stop doing all work. + private bool _disposed; + + public MessageBroker(IPerformanceCounterManager performanceCounterManager) + : this(performanceCounterManager, 3 * Environment.ProcessorCount, Environment.ProcessorCount) + { + } + + public MessageBroker(IPerformanceCounterManager performanceCounterManager, int maxWorkers, int maxIdleWorkers) + { + _counters = performanceCounterManager; + _maxWorkers = maxWorkers; + _maxIdleWorkers = maxIdleWorkers; + } + + public TraceSource Trace + { + get; + set; + } + + public int AllocatedWorkers + { + get + { + return _allocatedWorkers; + } + } + + public int BusyWorkers + { + get + { + return _busyWorkers; + } + } + + public void Schedule(ISubscription subscription) + { + if (subscription == null) + { + throw new ArgumentNullException("subscription"); + } + + if (_disposed) + { + // Don't queue up new work if we've disposed the broker + return; + } + + if (subscription.SetQueued()) + { + lock (_queue) + { + _queue.Enqueue(subscription); + Monitor.Pulse(_queue); + AddWorker(); + } + } + } + + private void AddWorker() + { + // Only create a new worker if everyone is busy (up to the max) + if (_allocatedWorkers < _maxWorkers) + { + if (_allocatedWorkers == _busyWorkers) + { + _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Increment(ref _allocatedWorkers); + + Trace.TraceEvent(TraceEventType.Verbose, 0, "Creating a worker, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); + + ThreadPool.QueueUserWorkItem(ProcessWork); + } + else + { + Trace.TraceEvent(TraceEventType.Verbose, 0, "No need to add a worker because all allocated workers are not busy, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); + } + } + else + { + Trace.TraceEvent(TraceEventType.Verbose, 0, "Already at max workers, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers); + } + } + + private void ProcessWork(object state) + { + Task pumpTask = PumpAsync(); + + if (pumpTask.IsCompleted) + { + ProcessWorkSync(pumpTask); + } + else + { + ProcessWorkAsync(pumpTask); + } + + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] + private void ProcessWorkSync(Task pumpTask) + { + try + { + pumpTask.Wait(); + } + catch (Exception ex) + { + Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + ex.GetBaseException()); + } + finally + { + // After the pump runs decrement the number of workers in flight + _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers); + } + } + + private void ProcessWorkAsync(Task pumpTask) + { + pumpTask.ContinueWith(task => + { + // After the pump runs decrement the number of workers in flight + _counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers); + + if (task.IsFaulted) + { + Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + task.Exception.GetBaseException()); + } + }); + } + + private Task PumpAsync() + { + var tcs = new TaskCompletionSource(); + PumpImpl(tcs); + return tcs.Task; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] + private void PumpImpl(TaskCompletionSource taskCompletionSource, ISubscription subscription = null) + { + + Process: + // If we were doing work before and now we've been disposed just kill this worker early + if (_disposed) + { + taskCompletionSource.TrySetResult(null); + return; + } + + Debug.Assert(_allocatedWorkers <= _maxWorkers, "How did we pass the max?"); + + // If we're withing the acceptable limit of idleness, just keep running + int idleWorkers = _allocatedWorkers - _busyWorkers; + + if (subscription != null || idleWorkers <= _maxIdleWorkers) + { + // We already have a subscription doing work so skip the queue + if (subscription == null) + { + lock (_queue) + { + while (_queue.Count == 0) + { + Monitor.Wait(_queue); + + // When disposing, all workers are pulsed so that they can quit + // if they're waiting for things to do (idle) + if (_disposed) + { + taskCompletionSource.TrySetResult(null); + return; + } + } + + subscription = _queue.Dequeue(); + } + } + + _counters.MessageBusBusyWorkers.RawValue = Interlocked.Increment(ref _busyWorkers); + + Task workTask = subscription.Work(); + + if (workTask.IsCompleted) + { + try + { + workTask.Wait(); + + goto Process; + } + catch (Exception ex) + { + Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + ex.GetBaseException()); + + goto Process; + } + finally + { + if (!subscription.UnsetQueued() || workTask.IsFaulted) + { + // If we don't have more work to do just make the subscription null + subscription = null; + } + + _counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers); + + Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative"); + } + } + else + { + PumpImplAsync(workTask, subscription, taskCompletionSource); + } + } + else + { + taskCompletionSource.TrySetResult(null); + } + } + + private void PumpImplAsync(Task workTask, ISubscription subscription, TaskCompletionSource taskCompletionSource) + { + // Async path + workTask.ContinueWith(task => + { + bool moreWork = subscription.UnsetQueued(); + + _counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers); + + Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative"); + + if (task.IsFaulted) + { + Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + task.Exception.GetBaseException()); + } + + if (moreWork && !task.IsFaulted) + { + PumpImpl(taskCompletionSource, subscription); + } + else + { + // Don't reference the subscription anymore + subscription = null; + + PumpImpl(taskCompletionSource); + } + }); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (!_disposed) + { + _disposed = true; + + Trace.TraceEvent(TraceEventType.Verbose, 0, "Dispoing the broker"); + + // Wait for all threads to stop working + WaitForDrain(); + + Trace.TraceEvent(TraceEventType.Verbose, 0, "Disposed the broker"); + } + } + } + + public void Dispose() + { + Dispose(true); + } + + private void WaitForDrain() + { + while (_allocatedWorkers > 0) + { + lock (_queue) + { + // Tell all workers we're done + Monitor.PulseAll(_queue); + } + + Thread.Sleep(250); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs new file mode 100644 index 000000000..c9692b03e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBus.cs @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Configuration; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// + /// + public class MessageBus : IMessageBus, IDisposable + { + private readonly MessageBroker _broker; + + // The size of the messages store we allocate per topic. + private readonly uint _messageStoreSize; + + // By default, topics are cleaned up after having no subscribers and after + // an interval based on the disconnect timeout has passed. While this works in normal cases + // it's an issue when the rate of incoming connections is too high. + // This is the maximum number of un-expired topics with no subscribers + // we'll leave hanging around. The rest will be cleaned up on an the gc interval. + private readonly int _maxTopicsWithNoSubscriptions; + + private readonly IStringMinifier _stringMinifier; + + private readonly ITraceManager _traceManager; + private readonly TraceSource _trace; + + private Timer _gcTimer; + private int _gcRunning; + private static readonly TimeSpan _gcInterval = TimeSpan.FromSeconds(5); + + private readonly TimeSpan _topicTtl; + + // For unit testing + internal Action BeforeTopicGarbageCollected; + internal Action AfterTopicGarbageCollected; + internal Action BeforeTopicMarked; + internal Action BeforeTopicCreated; + internal Action AfterTopicMarkedSuccessfully; + internal Action AfterTopicMarked; + + private const int DefaultMaxTopicsWithNoSubscriptions = 1000; + + private readonly Func _createTopic; + private readonly Action _addEvent; + private readonly Action _removeEvent; + private readonly Action _disposeSubscription; + + /// + /// + /// + /// + public MessageBus(IDependencyResolver resolver) + : this(resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve(), + DefaultMaxTopicsWithNoSubscriptions) + { + } + + /// + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The message broker is disposed when the bus is disposed.")] + public MessageBus(IStringMinifier stringMinifier, + ITraceManager traceManager, + IPerformanceCounterManager performanceCounterManager, + IConfigurationManager configurationManager, + int maxTopicsWithNoSubscriptions) + { + if (stringMinifier == null) + { + throw new ArgumentNullException("stringMinifier"); + } + + if (traceManager == null) + { + throw new ArgumentNullException("traceManager"); + } + + if (performanceCounterManager == null) + { + throw new ArgumentNullException("performanceCounterManager"); + } + + if (configurationManager == null) + { + throw new ArgumentNullException("configurationManager"); + } + + if (configurationManager.DefaultMessageBufferSize < 0) + { + throw new ArgumentOutOfRangeException(Resources.Error_BufferSizeOutOfRange); + } + + _stringMinifier = stringMinifier; + _traceManager = traceManager; + Counters = performanceCounterManager; + _trace = _traceManager["SignalR." + typeof(MessageBus).Name]; + _maxTopicsWithNoSubscriptions = maxTopicsWithNoSubscriptions; + + _gcTimer = new Timer(_ => GarbageCollectTopics(), state: null, dueTime: _gcInterval, period: _gcInterval); + + _broker = new MessageBroker(Counters) + { + Trace = _trace + }; + + // The default message store size + _messageStoreSize = (uint)configurationManager.DefaultMessageBufferSize; + + _topicTtl = configurationManager.TopicTtl(); + _createTopic = CreateTopic; + _addEvent = AddEvent; + _removeEvent = RemoveEvent; + _disposeSubscription = DisposeSubscription; + + Topics = new TopicLookup(); + } + + protected virtual TraceSource Trace + { + get + { + return _trace; + } + } + + protected internal TopicLookup Topics { get; private set; } + protected IPerformanceCounterManager Counters { get; private set; } + + public int AllocatedWorkers + { + get + { + return _broker.AllocatedWorkers; + } + } + + public int BusyWorkers + { + get + { + return _broker.BusyWorkers; + } + } + + /// + /// Publishes a new message to the specified event on the bus. + /// + /// The message to publish. + public virtual Task Publish(Message message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + Topic topic; + if (Topics.TryGetValue(message.Key, out topic)) + { + topic.Store.Add(message); + ScheduleTopic(topic); + } + + Counters.MessageBusMessagesPublishedTotal.Increment(); + Counters.MessageBusMessagesPublishedPerSec.Increment(); + + + return TaskAsyncHelper.Empty; + } + + protected ulong Save(Message message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + // GetTopic will return a topic for the given key. If topic exists and is Dying, + // it will revive it and mark it as NoSubscriptions + Topic topic = GetTopic(message.Key); + // Mark the topic as used so it doesn't immediately expire (if it was in that state before). + topic.MarkUsed(); + + return topic.Store.Add(message); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The disposable object is returned to the caller")] + public virtual IDisposable Subscribe(ISubscriber subscriber, string cursor, Func> callback, int maxMessages, object state) + { + if (subscriber == null) + { + throw new ArgumentNullException("subscriber"); + } + + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + Subscription subscription = CreateSubscription(subscriber, cursor, callback, maxMessages, state); + + // Set the subscription for this subscriber + subscriber.Subscription = subscription; + + var topics = new HashSet(); + + foreach (var key in subscriber.EventKeys) + { + // Create or retrieve topic and set it as HasSubscriptions + Topic topic = SubscribeTopic(key); + + // Set the subscription for this topic + subscription.SetEventTopic(key, topic); + + topics.Add(topic); + } + + subscriber.EventKeyAdded += _addEvent; + subscriber.EventKeyRemoved += _removeEvent; + subscriber.WriteCursor = subscription.WriteCursor; + + var subscriptionState = new SubscriptionState(subscriber); + var disposable = new DisposableAction(_disposeSubscription, subscriptionState); + + // When the subscription itself is disposed then dispose it + subscription.Disposable = disposable; + + // Add the subscription when it's all set and can be scheduled + // for work. It's important to do this after everything is wired up for the + // subscription so that publishes can schedule work at the right time. + foreach (var topic in topics) + { + topic.AddSubscription(subscription); + } + + subscriptionState.Initialized.Set(); + + // If there's a cursor then schedule work for this subscription + if (!String.IsNullOrEmpty(cursor)) + { + _broker.Schedule(subscription); + } + + return disposable; + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] + protected virtual Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func> callback, int messageBufferSize, object state) + { + return new DefaultSubscription(subscriber.Identity, subscriber.EventKeys, Topics, cursor, callback, messageBufferSize, _stringMinifier, Counters, state); + } + + protected void ScheduleEvent(string eventKey) + { + Topic topic; + if (Topics.TryGetValue(eventKey, out topic)) + { + ScheduleTopic(topic); + } + } + + private void ScheduleTopic(Topic topic) + { + try + { + topic.SubscriptionLock.EnterReadLock(); + + for (int i = 0; i < topic.Subscriptions.Count; i++) + { + ISubscription subscription = topic.Subscriptions[i]; + _broker.Schedule(subscription); + } + } + finally + { + topic.SubscriptionLock.ExitReadLock(); + } + } + + /// + /// Creates a topic for the specified key. + /// + /// The key to create the topic for. + /// A for the specified key. + protected virtual Topic CreateTopic(string key) + { + // REVIEW: This can be called multiple times, should we guard against it? + Counters.MessageBusTopicsCurrent.Increment(); + + return new Topic(_messageStoreSize, _topicTtl); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Stop the broker from doing any work + _broker.Dispose(); + + // Spin while we wait for the timer to finish if it's currently running + while (Interlocked.Exchange(ref _gcRunning, 1) == 1) + { + Thread.Sleep(250); + } + + // Remove all topics + Topics.Clear(); + + if (_gcTimer != null) + { + _gcTimer.Dispose(); + } + } + } + + public void Dispose() + { + Dispose(true); + } + + internal void GarbageCollectTopics() + { + if (Interlocked.Exchange(ref _gcRunning, 1) == 1) + { + return; + } + + int topicsWithNoSubs = 0; + + foreach (var pair in Topics) + { + if (pair.Value.IsExpired) + { + if (BeforeTopicGarbageCollected != null) + { + BeforeTopicGarbageCollected(pair.Key, pair.Value); + } + + // Mark the topic as dead + DestroyTopic(pair.Key, pair.Value); + } + else if (pair.Value.State == TopicState.NoSubscriptions) + { + // Keep track of the number of topics with no subscriptions + topicsWithNoSubs++; + } + } + + int overflow = topicsWithNoSubs - _maxTopicsWithNoSubscriptions; + if (overflow > 0) + { + // If we've overflowed the max the collect topics that don't have + // subscribers + var candidates = new List>(); + foreach (var pair in Topics) + { + if (pair.Value.State == TopicState.NoSubscriptions) + { + candidates.Add(pair); + } + } + + // We want to remove the overflow but oldest first + candidates.Sort((leftPair, rightPair) => leftPair.Value.LastUsed.CompareTo(rightPair.Value.LastUsed)); + + // Clear up to the overflow and stay within bounds + for (int i = 0; i < overflow && i < candidates.Count; i++) + { + var pair = candidates[i]; + // We only want to kill the topic if it's in the NoSubscriptions or Dying state. + if (InterlockedHelper.CompareExchangeOr(ref pair.Value.State, TopicState.Dead, TopicState.NoSubscriptions, TopicState.Dying)) + { + // Kill it + DestroyTopicCore(pair.Key, pair.Value); + } + } + } + + Interlocked.Exchange(ref _gcRunning, 0); + } + + private void DestroyTopic(string key, Topic topic) + { + // The goal of this function is to destroy topics after 2 garbage collect cycles + // This first if statement will transition a topic into the dying state on the first GC cycle + // but it will prevent the code path from hitting the second if statement + if (Interlocked.CompareExchange(ref topic.State, TopicState.Dying, TopicState.NoSubscriptions) == TopicState.Dying) + { + // If we've hit this if statement we're on the second GC cycle with this soon to be + // destroyed topic. At this point we move the Topic State into the Dead state as + // long as it has not been revived from the dying state. We check if the state is + // still dying again to ensure that the topic has not been transitioned into a new + // state since we've decided to destroy it. + if (Interlocked.CompareExchange(ref topic.State, TopicState.Dead, TopicState.Dying) == TopicState.Dying) + { + DestroyTopicCore(key, topic); + } + } + } + + private void DestroyTopicCore(string key, Topic topic) + { + Topics.TryRemove(key); + _stringMinifier.RemoveUnminified(key); + + Counters.MessageBusTopicsCurrent.Decrement(); + + Trace.TraceInformation("RemoveTopic(" + key + ")"); + + if (AfterTopicGarbageCollected != null) + { + AfterTopicGarbageCollected(key, topic); + } + } + + internal Topic GetTopic(string key) + { + Topic topic; + int oldState; + + do + { + if (BeforeTopicCreated != null) + { + BeforeTopicCreated(key); + } + + topic = Topics.GetOrAdd(key, _createTopic); + + if (BeforeTopicMarked != null) + { + BeforeTopicMarked(key, topic); + } + + // If the topic was dying revive it to the NoSubscriptions state. This is used to ensure + // that in the scaleout case that even if we're publishing to a topic with no subscriptions + // that we keep it around in case a user hops nodes. + oldState = Interlocked.CompareExchange(ref topic.State, TopicState.NoSubscriptions, TopicState.Dying); + + if (AfterTopicMarked != null) + { + AfterTopicMarked(key, topic, topic.State); + } + + // If the topic is currently dead then we're racing with the DestroyTopicCore function, therefore + // loop around until we're able to create a new topic + } while (oldState == TopicState.Dead); + + if (AfterTopicMarkedSuccessfully != null) + { + AfterTopicMarkedSuccessfully(key, topic); + } + + return topic; + } + + internal Topic SubscribeTopic(string key) + { + Topic topic; + + do + { + if (BeforeTopicCreated != null) + { + BeforeTopicCreated(key); + } + + topic = Topics.GetOrAdd(key, _createTopic); + + if (BeforeTopicMarked != null) + { + BeforeTopicMarked(key, topic); + } + + // Transition into the HasSubscriptions state as long as the topic is not dead + InterlockedHelper.CompareExchangeOr(ref topic.State, TopicState.HasSubscriptions, TopicState.NoSubscriptions, TopicState.Dying); + + if (AfterTopicMarked != null) + { + AfterTopicMarked(key, topic, topic.State); + } + + // If we were unable to transition into the HasSubscription state that means we're in the Dead state. + // Loop around until we're able to create the topic new + } while (topic.State != TopicState.HasSubscriptions); + + if (AfterTopicMarkedSuccessfully != null) + { + AfterTopicMarkedSuccessfully(key, topic); + } + + return topic; + } + + private void AddEvent(ISubscriber subscriber, string eventKey) + { + Topic topic = SubscribeTopic(eventKey); + + // Add or update the cursor (in case it already exists) + if (subscriber.Subscription.AddEvent(eventKey, topic)) + { + // Add it to the list of subs + topic.AddSubscription(subscriber.Subscription); + } + } + + private void RemoveEvent(ISubscriber subscriber, string eventKey) + { + Topic topic; + if (Topics.TryGetValue(eventKey, out topic)) + { + topic.RemoveSubscription(subscriber.Subscription); + subscriber.Subscription.RemoveEvent(eventKey); + } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Failure to invoke the callback should be ignored")] + private void DisposeSubscription(object state) + { + var subscriptionState = (SubscriptionState)state; + var subscriber = subscriptionState.Subscriber; + + // This will stop work from continuting to happen + subscriber.Subscription.Dispose(); + + try + { + // Invoke the terminal callback + subscriber.Subscription.Invoke(MessageResult.TerminalMessage).Wait(); + } + catch + { + // We failed to talk to the subscriber because they are already gone + // so the terminal message isn't required. + } + + subscriptionState.Initialized.Wait(); + + subscriber.EventKeyAdded -= _addEvent; + subscriber.EventKeyRemoved -= _removeEvent; + subscriber.WriteCursor = null; + + for (int i = subscriber.EventKeys.Count - 1; i >= 0; i--) + { + string eventKey = subscriber.EventKeys[i]; + RemoveEvent(subscriber, eventKey); + } + } + + private class SubscriptionState + { + public ISubscriber Subscriber { get; private set; } + public ManualResetEventSlim Initialized { get; private set; } + + public SubscriptionState(ISubscriber subscriber) + { + Initialized = new ManualResetEventSlim(); + Subscriber = subscriber; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs new file mode 100644 index 000000000..874a314e1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageBusExtensions.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public static class MessageBusExtensions + { + public static Task Publish(this IMessageBus bus, string source, string key, string value) + { + if (bus == null) + { + throw new ArgumentNullException("bus"); + } + + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (String.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key"); + } + + return bus.Publish(new Message(source, key, value)); + } + + internal static Task Ack(this IMessageBus bus, string connectionId, string commandId) + { + // Prepare the ack + var message = new Message(connectionId, PrefixHelper.GetAck(connectionId), null); + message.CommandId = commandId; + message.IsAck = true; + return bus.Publish(message); + } + + public static void Enumerate(this IList> messages, Action onMessage) + { + if (messages == null) + { + throw new ArgumentNullException("messages"); + } + + if (onMessage == null) + { + throw new ArgumentNullException("onMessage"); + } + + Enumerate(messages, message => true, (state, message) => onMessage(message), state: null); + } + + public static void Enumerate(this IList> messages, Func filter, Action onMessage, T state) + { + if (messages == null) + { + throw new ArgumentNullException("messages"); + } + + if (filter == null) + { + throw new ArgumentNullException("filter"); + } + + if (onMessage == null) + { + throw new ArgumentNullException("onMessage"); + } + + for (int i = 0; i < messages.Count; i++) + { + ArraySegment segment = messages[i]; + for (int j = segment.Offset; j < segment.Offset + segment.Count; j++) + { + Message message = segment.Array[j]; + + if (filter(message)) + { + onMessage(state, message); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs new file mode 100644 index 000000000..31a68beb9 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageResult.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Messages are never compared")] + public struct MessageResult + { + private static readonly List> _emptyList = new List>(); + public readonly static MessageResult TerminalMessage = new MessageResult(terminal: true); + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization to avoid allocations.")] + public IList> Messages { get; private set; } + + public int TotalCount { get; private set; } + + public bool Terminal { get; set; } + + public MessageResult(bool terminal) + : this(_emptyList, 0) + { + Terminal = terminal; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The array of messages associated with this . + /// The amount of messages populated in the messages array. + public MessageResult(IList> messages, int totalCount) + : this() + { + Messages = messages; + TotalCount = totalCount; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs new file mode 100644 index 000000000..565907f4a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStore.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + // Represents a message store that is backed by a ring buffer. + public sealed class MessageStore where T : class + { + private static readonly uint _minFragmentCount = 4; + private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH + private static readonly ArraySegment _emptyArraySegment = new ArraySegment(new T[0]); + private readonly uint _offset; + + private Fragment[] _fragments; + private readonly uint _fragmentSize; + + private long _nextFreeMessageId; + + // Creates a message store with the specified capacity. The actual capacity will be *at least* the + // specified value. That is, GetMessages may return more data than 'capacity'. + public MessageStore(uint capacity, uint offset) + { + // set a minimum capacity + if (capacity < 32) + { + capacity = 32; + } + + _offset = offset; + + // Dynamically choose an appropriate number of fragments and the size of each fragment. + // This is chosen to avoid allocations on the large object heap and to minimize contention + // in the store. We allocate a small amount of additional space to act as an overflow + // buffer; this increases throughput of the data structure. + checked + { + uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize); + _fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize); + _fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer + } + } + + public MessageStore(uint capacity) + : this(capacity, offset: 0) + { + } + + // only for testing purposes + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Only for testing")] + public ulong GetMessageCount() + { + return (ulong)Volatile.Read(ref _nextFreeMessageId); + } + + // Adds a message to the store. Returns the ID of the newly added message. + public ulong Add(T message) + { + // keep looping in TryAddImpl until it succeeds + ulong newMessageId; + while (!TryAddImpl(message, out newMessageId)) ; + + // When TryAddImpl succeeds, record the fact that a message was just added to the + // store. We increment the next free id rather than set it explicitly since + // multiple threads might be trying to write simultaneously. There is a nifty + // side effect to this: _nextFreeMessageId will *always* return the total number + // of messages that *all* threads agree have ever been added to the store. (The + // actual number may be higher, but this field will eventually catch up as threads + // flush data.) + Interlocked.Increment(ref _nextFreeMessageId); + return newMessageId; + } + + private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment) + { + fragmentNum = messageId / _fragmentSize; + + // from the bucket number, we can figure out where in _fragments this data sits + idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length); + idxIntoFragment = (int)(messageId % _fragmentSize); + } + + private ulong GetMessageId(ulong fragmentNum, uint offset) + { + return fragmentNum * _fragmentSize + offset; + } + + // Gets the next batch of messages, beginning with the specified ID. + // This function may return an empty array or an array of length greater than the capacity + // specified in the ctor. The client may also miss messages. See MessageStoreResult. + public MessageStoreResult GetMessages(ulong firstMessageId, int maxMessages) + { + return GetMessagesImpl(firstMessageId, maxMessages); + } + + private MessageStoreResult GetMessagesImpl(ulong firstMessageIdRequestedByClient, int maxMessages) + { + ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + + // Case 1: + // The client is already up-to-date with the message store, so we return no data. + if (nextFreeMessageId <= firstMessageIdRequestedByClient) + { + return new MessageStoreResult(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false); + } + + // look for the fragment containing the start of the data requested by the client + ulong fragmentNum; + int idxIntoFragmentsArray, idxIntoFragment; + GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment thisFragment = _fragments[idxIntoFragmentsArray]; + ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: _offset); + ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize; + + // Case 2: + // This fragment contains the first part of the data the client requested. + if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment) + { + int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient); + + // Limit the number of messages the caller sees + count = Math.Min(count, maxMessages); + + ArraySegment retMessages = new ArraySegment(thisFragment.Data, idxIntoFragment, count); + + return new MessageStoreResult(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment)); + } + + // Case 3: + // The client has missed messages, so we need to send him the earliest fragment we have. + while (true) + { + GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length]; + if (tailFragment.FragmentNum < fragmentNum) + { + firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: _offset); + int count = Math.Min(maxMessages, tailFragment.Data.Length); + return new MessageStoreResult(firstMessageIdInThisFragment, new ArraySegment(tailFragment.Data, 0, count), hasMoreData: true); + } + nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + } + } + + private bool TryAddImpl(T message, out ulong newMessageId) + { + ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + + // locate the fragment containing the next free id, which is where we should write + ulong fragmentNum; + int idxIntoFragmentsArray, idxIntoFragment; + GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment fragment = _fragments[idxIntoFragmentsArray]; + + if (fragment == null || fragment.FragmentNum < fragmentNum) + { + // the fragment is outdated (or non-existent) and must be replaced + + if (idxIntoFragment == 0) + { + // this thread is responsible for creating the fragment + Fragment newFragment = new Fragment(fragmentNum, _fragmentSize); + newFragment.Data[0] = message; + Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment); + if (existingFragment == fragment) + { + newMessageId = GetMessageId(fragmentNum, offset: _offset); + return true; + } + } + + // another thread is responsible for updating the fragment, so fall to bottom of method + } + else if (fragment.FragmentNum == fragmentNum) + { + // the fragment is valid, and we can just try writing into it until we reach the end of the fragment + T[] fragmentData = fragment.Data; + for (int i = idxIntoFragment; i < fragmentData.Length; i++) + { + T originalMessage = Interlocked.CompareExchange(ref fragmentData[i], message, null); + if (originalMessage == null) + { + newMessageId = GetMessageId(fragmentNum, offset: (uint)i); + return true; + } + } + + // another thread used the last open space in this fragment, so fall to bottom of method + } + + // failure; caller will retry operation + newMessageId = 0; + return false; + } + + private sealed class Fragment + { + public readonly ulong FragmentNum; + public readonly T[] Data; + + public Fragment(ulong fragmentNum, uint fragmentSize) + { + FragmentNum = fragmentNum; + Data = new T[fragmentSize]; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs new file mode 100644 index 000000000..665c887d4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/MessageStoreResult.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + // Represents the result of a call to MessageStore.GetMessages. + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "This is never compared")] + public struct MessageStoreResult where T : class + { + // The first message ID in the result set. Messages in the result set have sequentually increasing IDs. + // If FirstMessageId = 20 and Messages.Length = 4, then the messages have IDs { 20, 21, 22, 23 }. + private readonly ulong _firstMessageId; + + // If this is true, the backing MessageStore contains more messages, and the client should call GetMessages again. + private readonly bool _hasMoreData; + + // The actual result set. May be empty. + private readonly ArraySegment _messages; + + public MessageStoreResult(ulong firstMessageId, ArraySegment messages, bool hasMoreData) + { + _firstMessageId = firstMessageId; + _messages = messages; + _hasMoreData = hasMoreData; + } + + public ulong FirstMessageId + { + get + { + return _firstMessageId; + } + } + + public bool HasMoreData + { + get + { + return _hasMoreData; + } + } + + public ArraySegment Messages + { + get + { + return _messages; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs new file mode 100644 index 000000000..1ac31f354 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutConfiguration.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// Common settings for scale-out message bus implementations. + /// + public class ScaleoutConfiguration + { + public static readonly int DisableQueuing = 0; + + private int _maxQueueLength; + + /// + /// The maximum length of the outgoing send queue. Messages being sent to the backplane are queued + /// up to this length. After the max length is reached, further sends will throw an InvalidOperationException. + /// Set to ScaleoutConfiguration.DisableQueuing to disable queing. + /// Defaults to disabled. + /// + public virtual int MaxQueueLength + { + get + { + return _maxQueueLength; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value"); + } + + _maxQueueLength = value; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs new file mode 100644 index 000000000..15fefa51c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMapping.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class ScaleoutMapping + { + public ScaleoutMapping(ulong id, ScaleoutMessage message) + : this(id, message, ListHelper.Empty) + { + } + + public ScaleoutMapping(ulong id, ScaleoutMessage message, IList localKeyInfo) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + if (localKeyInfo == null) + { + throw new ArgumentNullException("localKeyInfo"); + } + + Id = id; + LocalKeyInfo = localKeyInfo; + ServerCreationTime = message.ServerCreationTime; + } + + public ulong Id { get; private set; } + public IList LocalKeyInfo { get; private set; } + public DateTime ServerCreationTime { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs new file mode 100644 index 000000000..c98a702a7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMappingStore.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class ScaleoutMappingStore + { + private const int MaxMessages = 1000000; + + private ScaleoutStore _store; + + public ScaleoutMappingStore() + { + _store = new ScaleoutStore(MaxMessages); + } + + public void Add(ulong id, ScaleoutMessage message, IList localKeyInfo) + { + if (MaxMapping != null && id < MaxMapping.Id) + { + _store = new ScaleoutStore(MaxMessages); + } + + _store.Add(new ScaleoutMapping(id, message, localKeyInfo)); + } + + public ScaleoutMapping MaxMapping + { + get + { + return _store.MaxMapping; + } + } + + public IEnumerator GetEnumerator(ulong id) + { + MessageStoreResult result = _store.GetMessagesByMappingId(id); + + return new ScaleoutStoreEnumerator(_store, result); + } + + private struct ScaleoutStoreEnumerator : IEnumerator, IEnumerator + { + private readonly WeakReference _storeReference; + private MessageStoreResult _result; + private int _offset; + private int _length; + private ulong _nextId; + + public ScaleoutStoreEnumerator(ScaleoutStore store, MessageStoreResult result) + : this() + { + _storeReference = new WeakReference(store); + Initialize(result); + } + + public ScaleoutMapping Current + { + get + { + return _result.Messages.Array[_offset]; + } + } + + public void Dispose() + { + + } + + object IEnumerator.Current + { + get { return Current; } + } + + public bool MoveNext() + { + _offset++; + + if (_offset < _length) + { + return true; + } + + if (!_result.HasMoreData) + { + return false; + } + + // If the store falls out of scope + var store = (ScaleoutStore)_storeReference.Target; + + if (store == null) + { + return false; + } + + // Get the next result + MessageStoreResult result = store.GetMessages(_nextId); + Initialize(result); + + _offset++; + + return _offset < _length; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + private void Initialize(MessageStoreResult result) + { + _result = result; + _offset = _result.Messages.Offset - 1; + _length = _result.Messages.Offset + _result.Messages.Count; + _nextId = _result.FirstMessageId + (ulong)_result.Messages.Count; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs new file mode 100644 index 000000000..88643efb4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessage.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// Represents a message to the scaleout backplane + /// + public class ScaleoutMessage + { + public ScaleoutMessage(IList messages) + { + Messages = messages; + ServerCreationTime = DateTime.UtcNow; + } + + public ScaleoutMessage() + { + } + + /// + /// The messages from SignalR + /// + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is used for serialization")] + public IList Messages { get; set; } + + /// + /// The time the message was created on the origin server + /// + public DateTime ServerCreationTime { get; set; } + + public byte[] ToBytes() + { + using (var ms = new MemoryStream()) + { + var binaryWriter = new BinaryWriter(ms); + + binaryWriter.Write(Messages.Count); + for (int i = 0; i < Messages.Count; i++) + { + Messages[i].WriteTo(ms); + } + binaryWriter.Write(ServerCreationTime.Ticks); + + return ms.ToArray(); + } + } + + public static ScaleoutMessage FromBytes(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException("data"); + } + + using (var stream = new MemoryStream(data)) + { + var binaryReader = new BinaryReader(stream); + var message = new ScaleoutMessage(); + message.Messages = new List(); + int count = binaryReader.ReadInt32(); + for (int i = 0; i < count; i++) + { + message.Messages.Add(Message.ReadFrom(stream)); + } + message.ServerCreationTime = new DateTime(binaryReader.ReadInt64()); + + return message; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs new file mode 100644 index 000000000..7d9fbe10d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutMessageBus.cs @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + /// + /// Common base class for scaleout message bus implementations. + /// + public abstract class ScaleoutMessageBus : MessageBus + { + private readonly SipHashBasedStringEqualityComparer _sipHashBasedComparer = new SipHashBasedStringEqualityComparer(0, 0); + private readonly TraceSource _trace; + private readonly Lazy _streamManager; + private readonly IPerformanceCounterManager _perfCounters; + + protected ScaleoutMessageBus(IDependencyResolver resolver, ScaleoutConfiguration configuration) + : base(resolver) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + var traceManager = resolver.Resolve(); + _trace = traceManager["SignalR." + typeof(ScaleoutMessageBus).Name]; + _perfCounters = resolver.Resolve(); + _streamManager = new Lazy(() => new ScaleoutStreamManager(Send, OnReceivedCore, StreamCount, _trace, _perfCounters, configuration)); + } + + /// + /// The number of streams can't change for the lifetime of this instance. + /// + protected virtual int StreamCount + { + get + { + return 1; + } + } + + private ScaleoutStreamManager StreamManager + { + get + { + return _streamManager.Value; + } + } + + /// + /// Opens the specified queue for sending messages. + /// The index of the stream to open. + /// + protected void Open(int streamIndex) + { + StreamManager.Open(streamIndex); + } + + /// + /// Closes the specified queue. + /// The index of the stream to close. + /// + protected void Close(int streamIndex) + { + StreamManager.Close(streamIndex); + } + + /// + /// Closes the specified queue for sending messages making all sends fail asynchronously. + /// + /// The index of the stream to close. + /// The error that occurred. + protected void OnError(int streamIndex, Exception exception) + { + StreamManager.OnError(streamIndex, exception); + } + + /// + /// Sends messages to the backplane + /// + /// The list of messages to send + /// + protected virtual Task Send(IList messages) + { + // If we're only using a single stream then just send + if (StreamCount == 1) + { + return StreamManager.Send(0, messages); + } + + var taskCompletionSource = new TaskCompletionSource(); + + // Group messages by source (connection id) + var messagesBySource = messages.GroupBy(m => m.Source); + + SendImpl(messagesBySource.GetEnumerator(), taskCompletionSource); + + return taskCompletionSource.Task; + } + + protected virtual Task Send(int streamIndex, IList messages) + { + throw new NotImplementedException(); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We return a faulted tcs")] + private void SendImpl(IEnumerator> enumerator, TaskCompletionSource taskCompletionSource) + { + send: + + if (!enumerator.MoveNext()) + { + taskCompletionSource.TrySetResult(null); + } + else + { + IGrouping group = enumerator.Current; + + // Get the channel index we're going to use for this message + int index = (int)((uint)_sipHashBasedComparer.GetHashCode(group.Key) % StreamCount); + + Debug.Assert(index >= 0, "Hash function resulted in an index < 0."); + + Task sendTask = StreamManager.Send(index, group.ToArray()).Catch(); + + if (sendTask.IsCompleted) + { + try + { + sendTask.Wait(); + + goto send; + + } + catch (Exception ex) + { + taskCompletionSource.SetUnwrappedException(ex); + } + } + else + { + sendTask.Then((enumer, tcs) => SendImpl(enumer, tcs), enumerator, taskCompletionSource) + .ContinueWithNotComplete(taskCompletionSource); + } + } + } + + /// + /// Invoked when a payload is received from the backplane. There should only be one active call at any time. + /// + /// id of the stream. + /// id of the payload within that stream. + /// The scaleout message. + /// + protected virtual void OnReceived(int streamIndex, ulong id, ScaleoutMessage message) + { + StreamManager.OnReceived(streamIndex, id, message); + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Justification = "Called from derived class")] + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] + private void OnReceivedCore(int streamIndex, ulong id, ScaleoutMessage scaleoutMessage) + { + Counters.ScaleoutMessageBusMessagesReceivedPerSec.IncrementBy(scaleoutMessage.Messages.Count); + + _trace.TraceInformation("OnReceived({0}, {1}, {2})", streamIndex, id, scaleoutMessage.Messages.Count); + + var localMapping = new LocalEventKeyInfo[scaleoutMessage.Messages.Count]; + var keys = new HashSet(); + + for (var i = 0; i < scaleoutMessage.Messages.Count; ++i) + { + Message message = scaleoutMessage.Messages[i]; + + // Remember where this message came from + message.MappingId = id; + message.StreamIndex = streamIndex; + + keys.Add(message.Key); + ulong localId = Save(message); + MessageStore messageStore = Topics[message.Key].Store; + + localMapping[i] = new LocalEventKeyInfo(message.Key, localId, messageStore); + } + + // Get the stream for this payload + ScaleoutMappingStore store = StreamManager.Streams[streamIndex]; + + // Publish only after we've setup the mapping fully + store.Add(id, scaleoutMessage, localMapping); + + // Schedule after we're done + foreach (var eventKey in keys) + { + ScheduleEvent(eventKey); + } + } + + public override Task Publish(Message message) + { + Counters.MessageBusMessagesPublishedTotal.Increment(); + Counters.MessageBusMessagesPublishedPerSec.Increment(); + + // TODO: Implement message batching here + return Send(new[] { message }); + } + + protected override void Dispose(bool disposing) + { + // Close all streams + for (int i = 0; i < StreamCount; i++) + { + Close(i); + } + + base.Dispose(disposing); + } + + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")] + protected override Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func> callback, int messageBufferSize, object state) + { + return new ScaleoutSubscription(subscriber.Identity, subscriber.EventKeys, cursor, StreamManager.Streams, callback, messageBufferSize, Counters, state); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs new file mode 100644 index 000000000..8d617d06a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStore.cs @@ -0,0 +1,441 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + // Represents a message store that is backed by a ring buffer. + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The rate sampler doesn't need to be disposed")] + public sealed class ScaleoutStore + { + private const uint _minFragmentCount = 4; + + [SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification = "It's conditional based on architecture")] + private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH + + private static readonly ArraySegment _emptyArraySegment = new ArraySegment(new ScaleoutMapping[0]); + + private Fragment[] _fragments; + private readonly uint _fragmentSize; + + private long _minMessageId; + private long _nextFreeMessageId; + + private ulong _minMappingId; + private ScaleoutMapping _maxMapping; + + // Creates a message store with the specified capacity. The actual capacity will be *at least* the + // specified value. That is, GetMessages may return more data than 'capacity'. + public ScaleoutStore(uint capacity) + { + // set a minimum capacity + if (capacity < 32) + { + capacity = 32; + } + + // Dynamically choose an appropriate number of fragments and the size of each fragment. + // This is chosen to avoid allocations on the large object heap and to minimize contention + // in the store. We allocate a small amount of additional space to act as an overflow + // buffer; this increases throughput of the data structure. + checked + { + uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize); + _fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize); + _fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer + } + } + + internal ulong MinMappingId + { + get + { + return _minMappingId; + } + } + + public ScaleoutMapping MaxMapping + { + get + { + return _maxMapping; + } + } + + public uint FragmentSize + { + get + { + return _fragmentSize; + } + } + + public int FragmentCount + { + get + { + return _fragments.Length; + } + } + + // Adds a message to the store. Returns the ID of the newly added message. + public ulong Add(ScaleoutMapping mapping) + { + // keep looping in TryAddImpl until it succeeds + ulong newMessageId; + while (!TryAddImpl(mapping, out newMessageId)) ; + + // When TryAddImpl succeeds, record the fact that a message was just added to the + // store. We increment the next free id rather than set it explicitly since + // multiple threads might be trying to write simultaneously. There is a nifty + // side effect to this: _nextFreeMessageId will *always* return the total number + // of messages that *all* threads agree have ever been added to the store. (The + // actual number may be higher, but this field will eventually catch up as threads + // flush data.) + Interlocked.Increment(ref _nextFreeMessageId); + return newMessageId; + } + + private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment) + { + fragmentNum = messageId / _fragmentSize; + + // from the bucket number, we can figure out where in _fragments this data sits + idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length); + idxIntoFragment = (int)(messageId % _fragmentSize); + } + + private int GetFragmentOffset(ulong messageId) + { + ulong fragmentNum = messageId / _fragmentSize; + + return (int)(fragmentNum % (uint)_fragments.Length); + } + + private ulong GetMessageId(ulong fragmentNum, uint offset) + { + return fragmentNum * _fragmentSize + offset; + } + + private bool TryAddImpl(ScaleoutMapping mapping, out ulong newMessageId) + { + ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + + // locate the fragment containing the next free id, which is where we should write + ulong fragmentNum; + int idxIntoFragmentsArray, idxIntoFragment; + GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment fragment = _fragments[idxIntoFragmentsArray]; + + if (fragment == null || fragment.FragmentNum < fragmentNum) + { + // the fragment is outdated (or non-existent) and must be replaced + bool overwrite = fragment != null && fragment.FragmentNum < fragmentNum; + + if (idxIntoFragment == 0) + { + // this thread is responsible for creating the fragment + Fragment newFragment = new Fragment(fragmentNum, _fragmentSize); + newFragment.Data[0] = mapping; + Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment); + if (existingFragment == fragment) + { + newMessageId = GetMessageId(fragmentNum, offset: 0); + newFragment.MinId = newMessageId; + newFragment.Length = 1; + newFragment.MaxId = GetMessageId(fragmentNum, offset: _fragmentSize - 1); + _maxMapping = mapping; + + // Move the minimum id when we overwrite + if (overwrite) + { + _minMessageId = (long)(existingFragment.MaxId + 1); + _minMappingId = existingFragment.MaxId; + } + else if (idxIntoFragmentsArray == 0) + { + _minMappingId = mapping.Id; + } + + return true; + } + } + + // another thread is responsible for updating the fragment, so fall to bottom of method + } + else if (fragment.FragmentNum == fragmentNum) + { + // the fragment is valid, and we can just try writing into it until we reach the end of the fragment + ScaleoutMapping[] fragmentData = fragment.Data; + for (int i = idxIntoFragment; i < fragmentData.Length; i++) + { + ScaleoutMapping originalMapping = Interlocked.CompareExchange(ref fragmentData[i], mapping, null); + if (originalMapping == null) + { + newMessageId = GetMessageId(fragmentNum, offset: (uint)i); + fragment.Length++; + _maxMapping = fragmentData[i]; + return true; + } + } + + // another thread used the last open space in this fragment, so fall to bottom of method + } + + // failure; caller will retry operation + newMessageId = 0; + return false; + } + + public MessageStoreResult GetMessages(ulong firstMessageIdRequestedByClient) + { + ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + + // Case 1: + // The client is already up-to-date with the message store, so we return no data. + if (nextFreeMessageId <= firstMessageIdRequestedByClient) + { + return new MessageStoreResult(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false); + } + + // look for the fragment containing the start of the data requested by the client + ulong fragmentNum; + int idxIntoFragmentsArray, idxIntoFragment; + GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment thisFragment = _fragments[idxIntoFragmentsArray]; + ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: 0); + ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize; + + // Case 2: + // This fragment contains the first part of the data the client requested. + if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment) + { + int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient); + + var retMessages = new ArraySegment(thisFragment.Data, idxIntoFragment, count); + + return new MessageStoreResult(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment)); + } + + // Case 3: + // The client has missed messages, so we need to send him the earliest fragment we have. + while (true) + { + GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length]; + if (tailFragment.FragmentNum < fragmentNum) + { + firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: 0); + + return new MessageStoreResult(firstMessageIdInThisFragment, new ArraySegment(tailFragment.Data, 0, tailFragment.Length), hasMoreData: true); + } + nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId); + } + } + + public MessageStoreResult GetMessagesByMappingId(ulong mappingId) + { + var minMessageId = (ulong)Volatile.Read(ref _minMessageId); + + int idxIntoFragment; + // look for the fragment containing the start of the data requested by the client + Fragment thisFragment; + if (TryGetFragmentFromMappingId(mappingId, out thisFragment)) + { + int lastSearchIndex; + ulong lastSearchId; + if (thisFragment.TrySearch(mappingId, + out idxIntoFragment, + out lastSearchIndex, + out lastSearchId)) + { + // Skip the first message + idxIntoFragment++; + ulong firstMessageIdRequestedByClient = GetMessageId(thisFragment.FragmentNum, (uint)idxIntoFragment); + + return GetMessages(firstMessageIdRequestedByClient); + } + else + { + if (mappingId > lastSearchId) + { + lastSearchIndex++; + } + + var segment = new ArraySegment(thisFragment.Data, + lastSearchIndex, + thisFragment.Length - lastSearchIndex); + + var firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: (uint)lastSearchIndex); + + return new MessageStoreResult(firstMessageIdInThisFragment, + segment, + hasMoreData: true); + } + } + + // If we're expired or we're at the first mapping or we're lower than the + // min then get everything + if (mappingId < _minMappingId || mappingId == UInt64.MaxValue) + { + return GetAllMessages(minMessageId); + } + + // We're up to date so do nothing + return new MessageStoreResult(0, _emptyArraySegment, hasMoreData: false); + } + + private MessageStoreResult GetAllMessages(ulong minMessageId) + { + ulong fragmentNum; + int idxIntoFragmentsArray, idxIntoFragment; + GetFragmentOffsets(minMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment); + + Fragment fragment = _fragments[idxIntoFragmentsArray]; + + if (fragment == null) + { + return new MessageStoreResult(minMessageId, _emptyArraySegment, hasMoreData: false); + } + + var firstMessageIdInThisFragment = GetMessageId(fragment.FragmentNum, offset: 0); + + var messages = new ArraySegment(fragment.Data, 0, fragment.Length); + + return new MessageStoreResult(firstMessageIdInThisFragment, messages, hasMoreData: true); + } + + internal bool TryGetFragmentFromMappingId(ulong mappingId, out Fragment fragment) + { + long low = _minMessageId; + long high = _nextFreeMessageId; + + while (low <= high) + { + var mid = (ulong)((low + high) / 2); + + int midOffset = GetFragmentOffset(mid); + + fragment = _fragments[midOffset]; + + if (fragment == null) + { + return false; + } + + if (mappingId < fragment.MinValue) + { + high = (long)(fragment.MinId - 1); + } + else if (mappingId > fragment.MaxValue) + { + low = (long)(fragment.MaxId + 1); + } + else if (fragment.HasValue(mappingId)) + { + return true; + } + } + + fragment = null; + return false; + } + + internal sealed class Fragment + { + public readonly ulong FragmentNum; + public readonly ScaleoutMapping[] Data; + public int Length; + public ulong MinId; + public ulong MaxId; + + public Fragment(ulong fragmentNum, uint fragmentSize) + { + FragmentNum = fragmentNum; + Data = new ScaleoutMapping[fragmentSize]; + } + + public ulong? MinValue + { + get + { + var mapping = Data[0]; + if (mapping != null) + { + return mapping.Id; + } + + return null; + } + } + + public ulong? MaxValue + { + get + { + ScaleoutMapping mapping = null; + + if (Length == 0) + { + mapping = Data[Length]; + } + else + { + mapping = Data[Length - 1]; + } + + if (mapping != null) + { + return mapping.Id; + } + + return null; + } + } + + public bool HasValue(ulong id) + { + return id >= MinValue && id <= MaxValue; + } + + public bool TrySearch(ulong id, out int index, out int lastSearchIndex, out ulong lastSearchId) + { + lastSearchIndex = 0; + lastSearchId = id; + + var low = 0; + var high = Length; + + + while (low <= high) + { + int mid = (low + high) / 2; + + ScaleoutMapping mapping = Data[mid]; + + lastSearchIndex = mid; + lastSearchId = mapping.Id; + + if (id < mapping.Id) + { + high = mid - 1; + } + else if (id > mapping.Id) + { + low = mid + 1; + } + else if (id == mapping.Id) + { + index = mid; + return true; + } + } + + index = -1; + return false; + } + } + } + +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs new file mode 100644 index 000000000..29c359a77 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStream.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + internal class ScaleoutStream + { + private TaskCompletionSource _taskCompletionSource; + private TaskQueue _queue; + private StreamState _state; + private Exception _error; + + private readonly int _size; + private readonly TraceSource _trace; + private readonly string _tracePrefix; + private readonly IPerformanceCounterManager _perfCounters; + + private readonly object _lockObj = new object(); + + public ScaleoutStream(TraceSource trace, string tracePrefix, int size, IPerformanceCounterManager performanceCounters) + { + if (trace == null) + { + throw new ArgumentNullException("trace"); + } + + _trace = trace; + _tracePrefix = tracePrefix; + _size = size; + _perfCounters = performanceCounters; + + InitializeCore(); + } + + private bool UsingTaskQueue + { + get + { + return _size > 0; + } + } + + public void Open() + { + lock (_lockObj) + { + if (ChangeState(StreamState.Open)) + { + _perfCounters.ScaleoutStreamCountOpen.Increment(); + _perfCounters.ScaleoutStreamCountBuffering.Decrement(); + + _error = null; + + if (UsingTaskQueue) + { + _taskCompletionSource.TrySetResult(null); + } + } + } + } + + public Task Send(Func send, object state) + { + lock (_lockObj) + { + if (_error != null) + { + throw _error; + } + + // If the queue is closed then stop sending + if (_state == StreamState.Closed) + { + throw new InvalidOperationException(Resources.Error_StreamClosed); + } + + if (_state == StreamState.Initial) + { + throw new InvalidOperationException(Resources.Error_StreamNotOpen); + } + + var context = new SendContext(this, send, state); + + if (UsingTaskQueue) + { + Task task = _queue.Enqueue(Send, context); + + if (task == null) + { + // The task is null if the queue is full + throw new InvalidOperationException(Resources.Error_TaskQueueFull); + } + + // Always observe the task in case the user doesn't handle it + return task.Catch(); + } + + _perfCounters.ScaleoutSendQueueLength.Increment(); + return Send(context).Finally(counter => + { + ((IPerformanceCounter)counter).Decrement(); + }, + _perfCounters.ScaleoutSendQueueLength); + } + } + + public void SetError(Exception error) + { + Trace("Error has happened with the following exception: {0}.", error); + + lock (_lockObj) + { + _perfCounters.ScaleoutErrorsTotal.Increment(); + _perfCounters.ScaleoutErrorsPerSec.Increment(); + + Buffer(); + + _error = error; + } + } + + public void Close() + { + Task task = TaskAsyncHelper.Empty; + + lock (_lockObj) + { + if (ChangeState(StreamState.Closed)) + { + _perfCounters.ScaleoutStreamCountOpen.RawValue = 0; + _perfCounters.ScaleoutStreamCountBuffering.RawValue = 0; + + if (UsingTaskQueue) + { + // Ensure the queue is started + EnsureQueueStarted(); + + // Drain the queue to stop all sends + task = Drain(_queue); + } + } + } + + if (UsingTaskQueue) + { + // Block until the queue is drained so no new work can be done + task.Wait(); + } + } + + private static Task Send(object state) + { + var context = (SendContext)state; + + context.InvokeSend().Then(tcs => + { + // Complete the task if the send is successful + tcs.TrySetResult(null); + }, + context.TaskCompletionSource) + .Catch((ex, obj) => + { + var ctx = (SendContext)obj; + + ctx.Stream.Trace("Send failed: {0}", ex); + + lock (ctx.Stream._lockObj) + { + // Set the queue into buffering state + ctx.Stream.SetError(ex.InnerException); + + // Otherwise just set this task as failed + ctx.TaskCompletionSource.TrySetUnwrappedException(ex); + } + }, + context); + + return context.TaskCompletionSource.Task; + } + + private void Buffer() + { + lock (_lockObj) + { + if (ChangeState(StreamState.Buffering)) + { + _perfCounters.ScaleoutStreamCountOpen.Decrement(); + _perfCounters.ScaleoutStreamCountBuffering.Increment(); + + InitializeCore(); + } + } + } + + private void InitializeCore() + { + if (UsingTaskQueue) + { + Task task = DrainQueue(); + _queue = new TaskQueue(task, _size); + _queue.QueueSizeCounter = _perfCounters.ScaleoutSendQueueLength; + } + } + + private Task DrainQueue() + { + // If the tcs is null or complete then create a new one + if (_taskCompletionSource == null || + _taskCompletionSource.Task.IsCompleted) + { + _taskCompletionSource = new TaskCompletionSource(); + } + + if (_queue != null) + { + // Drain the queue when the new queue is open + return _taskCompletionSource.Task.Then(q => Drain(q), _queue); + } + + // Nothing to drain + return _taskCompletionSource.Task; + } + + private void EnsureQueueStarted() + { + if (_taskCompletionSource != null) + { + _taskCompletionSource.TrySetResult(null); + } + } + + private bool ChangeState(StreamState newState) + { + // Do nothing if the state is closed + if (_state == StreamState.Closed) + { + return false; + } + + if (_state != newState) + { + Trace("Changed state from {0} to {1}", _state, newState); + + _state = newState; + return true; + } + + return false; + } + + private static Task Drain(TaskQueue queue) + { + if (queue == null) + { + return TaskAsyncHelper.Empty; + } + + var tcs = new TaskCompletionSource(); + + queue.Drain().Catch().ContinueWith(task => + { + tcs.SetResult(null); + }); + + return tcs.Task; + } + + private void Trace(string value, params object[] args) + { + _trace.TraceInformation(_tracePrefix + " - " + value, args); + } + + private class SendContext + { + private readonly Func _send; + private readonly object _state; + + public readonly ScaleoutStream Stream; + public readonly TaskCompletionSource TaskCompletionSource; + + public SendContext(ScaleoutStream stream, Func send, object state) + { + Stream = stream; + TaskCompletionSource = new TaskCompletionSource(); + _send = send; + _state = state; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception flows to the caller")] + public Task InvokeSend() + { + try + { + return _send(_state); + } + catch (Exception ex) + { + return TaskAsyncHelper.FromError(ex); + } + } + } + + private enum StreamState + { + Initial, + Open, + Buffering, + Closed + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs new file mode 100644 index 000000000..c0371ba6c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutStreamManager.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + internal class ScaleoutStreamManager + { + private readonly Func, Task> _send; + private readonly Action _receive; + private readonly ScaleoutStream[] _streams; + + public ScaleoutStreamManager(Func, Task> send, + Action receive, + int streamCount, + TraceSource trace, + IPerformanceCounterManager performanceCounters, + ScaleoutConfiguration configuration) + { + _streams = new ScaleoutStream[streamCount]; + _send = send; + _receive = receive; + + var receiveMapping = new ScaleoutMappingStore[streamCount]; + + performanceCounters.ScaleoutStreamCountTotal.RawValue = streamCount; + performanceCounters.ScaleoutStreamCountBuffering.RawValue = streamCount; + performanceCounters.ScaleoutStreamCountOpen.RawValue = 0; + + for (int i = 0; i < streamCount; i++) + { + _streams[i] = new ScaleoutStream(trace, "Stream(" + i + ")", configuration.MaxQueueLength, performanceCounters); + receiveMapping[i] = new ScaleoutMappingStore(); + } + + Streams = new ReadOnlyCollection(receiveMapping); + } + + public IList Streams { get; private set; } + + public void Open(int streamIndex) + { + _streams[streamIndex].Open(); + } + + public void Close(int streamIndex) + { + _streams[streamIndex].Close(); + } + + public void OnError(int streamIndex, Exception exception) + { + _streams[streamIndex].SetError(exception); + } + + public Task Send(int streamIndex, IList messages) + { + var context = new SendContext(this, streamIndex, messages); + + return _streams[streamIndex].Send(state => Send(state), context); + } + + public void OnReceived(int streamIndex, ulong id, ScaleoutMessage message) + { + _receive(streamIndex, id, message); + + // We assume if a message has come in then the stream is open + Open(streamIndex); + } + + private static Task Send(object state) + { + var context = (SendContext)state; + + return context.StreamManager._send(context.Index, context.Messages); + } + + private class SendContext + { + public ScaleoutStreamManager StreamManager; + public int Index; + public IList Messages; + + public SendContext(ScaleoutStreamManager scaleoutStream, int index, IList messages) + { + StreamManager = scaleoutStream; + Index = index; + Messages = messages; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs new file mode 100644 index 000000000..a8c34ade4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/ScaleoutSubscription.cs @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class ScaleoutSubscription : Subscription + { + private readonly IList _streams; + private readonly List _cursors; + + public ScaleoutSubscription(string identity, + IList eventKeys, + string cursor, + IList streams, + Func> callback, + int maxMessages, + IPerformanceCounterManager counters, + object state) + : base(identity, eventKeys, callback, maxMessages, counters, state) + { + if (streams == null) + { + throw new ArgumentNullException("streams"); + } + + _streams = streams; + + List cursors = null; + + if (String.IsNullOrEmpty(cursor)) + { + cursors = new List(); + } + else + { + cursors = Cursor.GetCursors(cursor); + + // If the streams don't match the cursors then throw it out + if (cursors.Count != _streams.Count) + { + cursors.Clear(); + } + } + + // No cursors so we need to populate them from the list of streams + if (cursors.Count == 0) + { + for (int streamIndex = 0; streamIndex < _streams.Count; streamIndex++) + { + AddCursorForStream(streamIndex, cursors); + } + } + + _cursors = cursors; + } + + public override void WriteCursor(TextWriter textWriter) + { + Cursor.WriteCursors(textWriter, _cursors); + } + + [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")] + [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")] + protected override void PerformWork(IList> items, out int totalCount, out object state) + { + // The list of cursors represent (streamid, payloadid) + var nextCursors = new ulong?[_cursors.Count]; + totalCount = 0; + + // Get the enumerator so that we can extract messages for this subscription + IEnumerator> enumerator = GetMappings().GetEnumerator(); + + while (totalCount < MaxMessages && enumerator.MoveNext()) + { + ScaleoutMapping mapping = enumerator.Current.Item1; + int streamIndex = enumerator.Current.Item2; + + ulong? nextCursor = nextCursors[streamIndex]; + + // Only keep going with this stream if the cursor we're looking at is bigger than + // anything we already processed + if (nextCursor == null || mapping.Id > nextCursor) + { + ulong mappingId = ExtractMessages(streamIndex, mapping, items, ref totalCount); + + // Update the cursor id + nextCursors[streamIndex] = mappingId; + } + } + + state = nextCursors; + } + + protected override void BeforeInvoke(object state) + { + // Update the list of cursors before invoking anything + var nextCursors = (ulong?[])state; + for (int i = 0; i < _cursors.Count; i++) + { + // Only update non-null entries + ulong? nextCursor = nextCursors[i]; + + if (nextCursor.HasValue) + { + Cursor cursor = _cursors[i]; + + cursor.Id = nextCursor.Value; + } + } + } + + private IEnumerable> GetMappings() + { + var enumerators = new List(); + + for (var streamIndex = 0; streamIndex < _streams.Count; ++streamIndex) + { + // Get the mapping for this stream + ScaleoutMappingStore store = _streams[streamIndex]; + + Cursor cursor = _cursors[streamIndex]; + + // Try to find a local mapping for this payload + var enumerator = new CachedStreamEnumerator(store.GetEnumerator(cursor.Id), + streamIndex); + + enumerators.Add(enumerator); + } + + while (enumerators.Count > 0) + { + ScaleoutMapping minMapping = null; + CachedStreamEnumerator minEnumerator = null; + + for (int i = enumerators.Count - 1; i >= 0; i--) + { + CachedStreamEnumerator enumerator = enumerators[i]; + + ScaleoutMapping mapping; + if (enumerator.TryMoveNext(out mapping)) + { + if (minMapping == null || mapping.ServerCreationTime < minMapping.ServerCreationTime) + { + minMapping = mapping; + minEnumerator = enumerator; + } + } + else + { + enumerators.RemoveAt(i); + } + } + + if (minMapping != null) + { + minEnumerator.ClearCachedValue(); + yield return Tuple.Create(minMapping, minEnumerator.StreamIndex); + } + } + } + + private ulong ExtractMessages(int streamIndex, ScaleoutMapping mapping, IList> items, ref int totalCount) + { + // For each of the event keys we care about, extract all of the messages + // from the payload + lock (EventKeys) + { + for (var i = 0; i < EventKeys.Count; ++i) + { + string eventKey = EventKeys[i]; + + for (int j = 0; j < mapping.LocalKeyInfo.Count; j++) + { + LocalEventKeyInfo info = mapping.LocalKeyInfo[j]; + + if (info.MessageStore != null && info.Key.Equals(eventKey, StringComparison.OrdinalIgnoreCase)) + { + MessageStoreResult storeResult = info.MessageStore.GetMessages(info.Id, 1); + + if (storeResult.Messages.Count > 0) + { + // TODO: Figure out what to do when we have multiple event keys per mapping + Message message = storeResult.Messages.Array[storeResult.Messages.Offset]; + + // Only add the message to the list if the stream index matches + if (message.StreamIndex == streamIndex) + { + items.Add(storeResult.Messages); + totalCount += storeResult.Messages.Count; + + // We got a mapping id bigger than what we expected which + // means we missed messages. Use the new mappingId. + if (message.MappingId > mapping.Id) + { + return message.MappingId; + } + } + else + { + // REVIEW: When the stream indexes don't match should we leave the mapping id as is? + // If we do nothing then we'll end up querying old cursor ids until + // we eventually find a message id that matches this stream index. + } + } + } + } + } + } + + return mapping.Id; + } + + private void AddCursorForStream(int streamIndex, List cursors) + { + ScaleoutMapping maxMapping = _streams[streamIndex].MaxMapping; + + ulong id = UInt64.MaxValue; + string key = streamIndex.ToString(CultureInfo.InvariantCulture); + + if (maxMapping != null) + { + id = maxMapping.Id; + } + + var newCursor = new Cursor(key, id); + cursors.Add(newCursor); + } + + private class CachedStreamEnumerator + { + private readonly IEnumerator _enumerator; + private ScaleoutMapping _cachedValue; + + public CachedStreamEnumerator(IEnumerator enumerator, int streamIndex) + { + _enumerator = enumerator; + StreamIndex = streamIndex; + } + + public int StreamIndex { get; private set; } + + public bool TryMoveNext(out ScaleoutMapping mapping) + { + mapping = null; + + if (_cachedValue != null) + { + mapping = _cachedValue; + return true; + } + + if (_enumerator.MoveNext()) + { + mapping = _enumerator.Current; + _cachedValue = mapping; + return true; + } + + return false; + } + + public void ClearCachedValue() + { + _cachedValue = null; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs new file mode 100644 index 000000000..825a13d1c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public abstract class Subscription : ISubscription, IDisposable + { + private readonly Func> _callback; + private readonly object _callbackState; + private readonly IPerformanceCounterManager _counters; + + private int _state; + private int _subscriptionState; + + private bool Alive + { + get + { + return _subscriptionState != SubscriptionState.Disposed; + } + } + + public string Identity { get; private set; } + + public IList EventKeys { get; private set; } + + public int MaxMessages { get; private set; } + + public IDisposable Disposable { get; set; } + + protected Subscription(string identity, IList eventKeys, Func> callback, int maxMessages, IPerformanceCounterManager counters, object state) + { + if (String.IsNullOrEmpty(identity)) + { + throw new ArgumentNullException("identity"); + } + + if (eventKeys == null) + { + throw new ArgumentNullException("eventKeys"); + } + + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + if (maxMessages < 0) + { + throw new ArgumentOutOfRangeException("maxMessages"); + } + + if (counters == null) + { + throw new ArgumentNullException("counters"); + } + + Identity = identity; + _callback = callback; + EventKeys = eventKeys; + MaxMessages = maxMessages; + _counters = counters; + _callbackState = state; + + _counters.MessageBusSubscribersTotal.Increment(); + _counters.MessageBusSubscribersCurrent.Increment(); + _counters.MessageBusSubscribersPerSec.Increment(); + } + + public virtual Task Invoke(MessageResult result) + { + return Invoke(result, state => { }, state: null); + } + + private Task Invoke(MessageResult result, Action beforeInvoke, object state) + { + // Change the state from idle to invoking callback + var prevState = Interlocked.CompareExchange(ref _subscriptionState, + SubscriptionState.InvokingCallback, + SubscriptionState.Idle); + + if (prevState == SubscriptionState.Disposed) + { + // Only allow terminal messages after dispose + if (!result.Terminal) + { + return TaskAsyncHelper.False; + } + } + + beforeInvoke(state); + + _counters.MessageBusMessagesReceivedTotal.IncrementBy(result.TotalCount); + _counters.MessageBusMessagesReceivedPerSec.IncrementBy(result.TotalCount); + + return _callback.Invoke(result, _callbackState).ContinueWith(task => + { + // Go from invoking callback to idle + Interlocked.CompareExchange(ref _subscriptionState, + SubscriptionState.Idle, + SubscriptionState.InvokingCallback); + return task; + }, + TaskContinuationOptions.ExecuteSynchronously).FastUnwrap(); + } + + public Task Work() + { + // Set the state to working + Interlocked.Exchange(ref _state, State.Working); + + var tcs = new TaskCompletionSource(); + + WorkImpl(tcs); + + // Fast Path + if (tcs.Task.IsCompleted) + { + return tcs.Task; + } + + return FinishAsync(tcs); + } + + public bool SetQueued() + { + return Interlocked.Increment(ref _state) == State.Working; + } + + public bool UnsetQueued() + { + // If we try to set the state to idle and we were not already in the working state then keep going + return Interlocked.CompareExchange(ref _state, State.Idle, State.Working) != State.Working; + } + + private static Task FinishAsync(TaskCompletionSource tcs) + { + return tcs.Task.ContinueWith(task => + { + if (task.IsFaulted) + { + return TaskAsyncHelper.FromError(task.Exception); + } + + return TaskAsyncHelper.Empty; + }).FastUnwrap(); + } + + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "We have a sync and async code path.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")] + private void WorkImpl(TaskCompletionSource taskCompletionSource) + { + Process: + if (!Alive) + { + // If this subscription is dead then return immediately + taskCompletionSource.TrySetResult(null); + return; + } + + var items = new List>(); + int totalCount; + object state; + + PerformWork(items, out totalCount, out state); + + if (items.Count > 0) + { + var messageResult = new MessageResult(items, totalCount); + Task callbackTask = Invoke(messageResult, s => BeforeInvoke(s), state); + + if (callbackTask.IsCompleted) + { + try + { + // Make sure exceptions propagate + callbackTask.Wait(); + + if (callbackTask.Result) + { + // Sync path + goto Process; + } + else + { + // If we're done pumping messages through to this subscription + // then dispose + Dispose(); + + // If the callback said it's done then stop + taskCompletionSource.TrySetResult(null); + } + } + catch (Exception ex) + { + taskCompletionSource.TrySetUnwrappedException(ex); + } + } + else + { + WorkImplAsync(callbackTask, taskCompletionSource); + } + } + else + { + taskCompletionSource.TrySetResult(null); + } + } + + protected virtual void BeforeInvoke(object state) + { + } + + [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")] + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "The caller wouldn't be able to specify what the generic type argument is")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "The count needs to be returned")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "The state needs to be set by the callee")] + protected abstract void PerformWork(IList> items, out int totalCount, out object state); + + private void WorkImplAsync(Task callbackTask, TaskCompletionSource taskCompletionSource) + { + // Async path + callbackTask.ContinueWith(task => + { + if (task.IsFaulted) + { + taskCompletionSource.TrySetUnwrappedException(task.Exception); + } + else if (task.Result) + { + WorkImpl(taskCompletionSource); + } + else + { + // If we're done pumping messages through to this subscription + // then dispose + Dispose(); + + // If the callback said it's done then stop + taskCompletionSource.TrySetResult(null); + } + }); + } + + public virtual bool AddEvent(string key, Topic topic) + { + return AddEventCore(key); + } + + public virtual void RemoveEvent(string key) + { + lock (EventKeys) + { + EventKeys.Remove(key); + } + } + + public virtual void SetEventTopic(string key, Topic topic) + { + // Don't call AddEvent since that's virtual + AddEventCore(key); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // REIVIEW: Consider sleeping instead of using a tight loop, or maybe timing out after some interval + // if the client is very slow then this invoke call might not end quickly and this will make the CPU + // hot waiting for the task to return. + + var spinWait = new SpinWait(); + + while (true) + { + // Wait until the subscription isn't working anymore + var state = Interlocked.CompareExchange(ref _subscriptionState, + SubscriptionState.Disposed, + SubscriptionState.Idle); + + // If we're not working then stop + if (state != SubscriptionState.InvokingCallback) + { + if (state != SubscriptionState.Disposed) + { + // Only decrement if we're not disposed already + _counters.MessageBusSubscribersCurrent.Decrement(); + _counters.MessageBusSubscribersPerSec.Decrement(); + } + + // Raise the disposed callback + if (Disposable != null) + { + Disposable.Dispose(); + } + + break; + } + + spinWait.SpinOnce(); + } + } + } + + public void Dispose() + { + Dispose(true); + } + + public abstract void WriteCursor(TextWriter textWriter); + + private bool AddEventCore(string key) + { + lock (EventKeys) + { + if (EventKeys.Contains(key)) + { + return false; + } + + EventKeys.Add(key); + return true; + } + } + + private static class State + { + public const int Idle = 0; + public const int Working = 1; + } + + private static class SubscriptionState + { + public const int Idle = 0; + public const int InvokingCallback = 1; + public const int Disposed = 2; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs new file mode 100644 index 000000000..eab7934c1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Topic.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public class Topic + { + private readonly TimeSpan _lifespan; + + // Keeps track of the last time this subscription was used + private DateTime _lastUsed = DateTime.UtcNow; + + public IList Subscriptions { get; private set; } + public MessageStore Store { get; private set; } + public ReaderWriterLockSlim SubscriptionLock { get; private set; } + + // State of the topic + internal int State; + + public virtual bool IsExpired + { + get + { + try + { + SubscriptionLock.EnterReadLock(); + + TimeSpan timeSpan = DateTime.UtcNow - _lastUsed; + + return Subscriptions.Count == 0 && timeSpan > _lifespan; + } + finally + { + SubscriptionLock.ExitReadLock(); + } + } + } + + public DateTime LastUsed + { + get + { + return _lastUsed; + } + } + + public Topic(uint storeSize, TimeSpan lifespan) + { + _lifespan = lifespan; + Subscriptions = new List(); + Store = new MessageStore(storeSize); + SubscriptionLock = new ReaderWriterLockSlim(); + } + + public void MarkUsed() + { + this._lastUsed = DateTime.UtcNow; + } + + public void AddSubscription(ISubscription subscription) + { + if (subscription == null) + { + throw new ArgumentNullException("subscription"); + } + + try + { + SubscriptionLock.EnterWriteLock(); + + MarkUsed(); + + Subscriptions.Add(subscription); + + // Created -> HasSubscriptions + Interlocked.CompareExchange(ref State, + TopicState.HasSubscriptions, + TopicState.NoSubscriptions); + } + finally + { + SubscriptionLock.ExitWriteLock(); + } + } + + public void RemoveSubscription(ISubscription subscription) + { + if (subscription == null) + { + throw new ArgumentNullException("subscription"); + } + + try + { + SubscriptionLock.EnterWriteLock(); + + MarkUsed(); + + Subscriptions.Remove(subscription); + + + if (Subscriptions.Count == 0) + { + // HasSubscriptions -> NoSubscriptions + Interlocked.CompareExchange(ref State, + TopicState.NoSubscriptions, + TopicState.HasSubscriptions); + } + } + finally + { + SubscriptionLock.ExitWriteLock(); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs new file mode 100644 index 000000000..d62d9084f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicLookup.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + public sealed class TopicLookup : IEnumerable> + { + // General topics + private readonly ConcurrentDictionary _topics = new ConcurrentDictionary(); + + // All group topics + private readonly ConcurrentDictionary _groupTopics = new ConcurrentDictionary(new SipHashBasedStringEqualityComparer()); + + public int Count + { + get + { + return _topics.Count + _groupTopics.Count; + } + } + + public Topic this[string key] + { + get + { + Topic topic; + if (TryGetValue(key, out topic)) + { + return topic; + } + return null; + } + } + + public bool ContainsKey(string key) + { + if (PrefixHelper.HasGroupPrefix(key)) + { + return _groupTopics.ContainsKey(key); + } + + return _topics.ContainsKey(key); + } + + public bool TryGetValue(string key, out Topic topic) + { + if (PrefixHelper.HasGroupPrefix(key)) + { + return _groupTopics.TryGetValue(key, out topic); + } + + return _topics.TryGetValue(key, out topic); + } + + public IEnumerator> GetEnumerator() + { + return _topics.Concat(_groupTopics).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool TryRemove(string key) + { + Topic topic; + if (PrefixHelper.HasGroupPrefix(key)) + { + return _groupTopics.TryRemove(key, out topic); + } + + return _topics.TryRemove(key, out topic); + } + + public Topic GetOrAdd(string key, Func factory) + { + if (PrefixHelper.HasGroupPrefix(key)) + { + return _groupTopics.GetOrAdd(key, factory); + } + + return _topics.GetOrAdd(key, factory); + } + + public void Clear() + { + _topics.Clear(); + _groupTopics.Clear(); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs new file mode 100644 index 000000000..e69193c89 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/TopicState.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Messaging +{ + internal class TopicState + { + public const int NoSubscriptions = 0; + public const int HasSubscriptions = 1; + public const int Dying = 2; + public const int Dead = 3; + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs b/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs new file mode 100644 index 000000000..e95726947 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Messaging/Volatile.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Messaging +{ + // All methods here are guaranteed both volatile + atomic. + // TODO: Make this use the .NET 4.5 'Volatile' type. + internal static class Volatile + { + public static long Read(ref long location) + { + return Interlocked.Read(ref location); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj new file mode 100644 index 000000000..5bab07857 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj @@ -0,0 +1,279 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} + Library + Properties + Microsoft.AspNet.SignalR + Microsoft.AspNet.SignalR.Core + 512 + true + ..\..\ + true + + + true + full + false + bin\Debug\ + TRACE;DEBUG;PERFCOUNTERS + prompt + 4 + bin\Debug\Microsoft.AspNet.SignalR.Core.XML + bin\Debug\Microsoft.AspNet.SignalR.Core.XML + + + pdbonly + true + bin\Release\ + TRACE;PERFCOUNTERS + prompt + 4 + bin\Release\Microsoft.AspNet.SignalR.Core.XML + + + + ..\packages\Microsoft.AspNet.SignalR.Client.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Client.dll + + + False + ..\packages\Newtonsoft.Json.5.0.8\lib\net40\Newtonsoft.Json.dll + + + + + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\CommonVersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings new file mode 100644 index 000000000..5b8822215 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Microsoft.AspNet.SignalR.Core.csproj.DotSettings @@ -0,0 +1,2 @@ + + DO_NOT_SHOW \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs b/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs new file mode 100644 index 000000000..ea6d96f35 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs @@ -0,0 +1,496 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Configuration; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; +using Microsoft.AspNet.SignalR.Tracing; +using Microsoft.AspNet.SignalR.Transports; + +namespace Microsoft.AspNet.SignalR +{ + /// + /// Represents a connection between client and server. + /// + public abstract class PersistentConnection + { + private const string WebSocketsTransportName = "webSockets"; + private static readonly char[] SplitChars = new[] { ':' }; + + private IConfigurationManager _configurationManager; + private ITransportManager _transportManager; + private bool _initialized; + private IServerCommandHandler _serverMessageHandler; + + public virtual void Initialize(IDependencyResolver resolver, HostContext context) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (_initialized) + { + return; + } + + MessageBus = resolver.Resolve(); + JsonSerializer = resolver.Resolve(); + TraceManager = resolver.Resolve(); + Counters = resolver.Resolve(); + AckHandler = resolver.Resolve(); + ProtectedData = resolver.Resolve(); + + _configurationManager = resolver.Resolve(); + _transportManager = resolver.Resolve(); + _serverMessageHandler = resolver.Resolve(); + + _initialized = true; + } + + public bool Authorize(IRequest request) + { + return AuthorizeRequest(request); + } + + protected virtual TraceSource Trace + { + get + { + return TraceManager["SignalR.PersistentConnection"]; + } + } + + protected IProtectedData ProtectedData { get; private set; } + + protected IMessageBus MessageBus { get; private set; } + + protected IJsonSerializer JsonSerializer { get; private set; } + + protected IAckHandler AckHandler { get; private set; } + + protected ITraceManager TraceManager { get; private set; } + + protected IPerformanceCounterManager Counters { get; private set; } + + protected ITransport Transport { get; private set; } + + /// + /// Gets the for the . + /// + public IConnection Connection + { + get; + private set; + } + + /// + /// Gets the for the . + /// + public IConnectionGroupManager Groups + { + get; + private set; + } + + private string DefaultSignal + { + get + { + return PrefixHelper.GetPersistentConnectionName(DefaultSignalRaw); + } + } + + private string DefaultSignalRaw + { + get + { + return GetType().FullName; + } + } + + internal virtual string GroupPrefix + { + get + { + return PrefixHelper.PersistentConnectionGroupPrefix; + } + } + + /// + /// Handles all requests for s. + /// + /// The for the current request. + /// A that completes when the pipeline is complete. + /// + /// Thrown if connection wasn't initialized. + /// Thrown if the transport wasn't specified. + /// Thrown if the connection id wasn't specified. + /// + public virtual Task ProcessRequest(HostContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (!_initialized) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_ConnectionNotInitialized)); + } + + if (IsNegotiationRequest(context.Request)) + { + return ProcessNegotiationRequest(context); + } + else if (IsPingRequest(context.Request)) + { + return ProcessPingRequest(context); + } + + Transport = GetTransport(context); + + if (Transport == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorUnknownTransport)); + } + + string connectionToken = context.Request.QueryString["connectionToken"]; + + // If there's no connection id then this is a bad request + if (String.IsNullOrEmpty(connectionToken)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorMissingConnectionToken)); + } + + string connectionId = GetConnectionId(context, connectionToken); + + // Set the transport's connection id to the unprotected one + Transport.ConnectionId = connectionId; + + IList signals = GetSignals(connectionId); + IList groups = AppendGroupPrefixes(context, connectionId); + + Connection connection = CreateConnection(connectionId, signals, groups); + + Connection = connection; + string groupName = PrefixHelper.GetPersistentConnectionGroupName(DefaultSignalRaw); + Groups = new GroupManager(connection, groupName); + + Transport.TransportConnected = () => + { + var command = new ServerCommand + { + ServerCommandType = ServerCommandType.RemoveConnection, + Value = connectionId + }; + + return _serverMessageHandler.SendCommand(command); + }; + + Transport.Connected = () => + { + return TaskAsyncHelper.FromMethod(() => OnConnected(context.Request, connectionId).OrEmpty()); + }; + + Transport.Reconnected = () => + { + return TaskAsyncHelper.FromMethod(() => OnReconnected(context.Request, connectionId).OrEmpty()); + }; + + Transport.Received = data => + { + Counters.ConnectionMessagesSentTotal.Increment(); + Counters.ConnectionMessagesSentPerSec.Increment(); + return TaskAsyncHelper.FromMethod(() => OnReceived(context.Request, connectionId, data).OrEmpty()); + }; + + Transport.Disconnected = () => + { + return TaskAsyncHelper.FromMethod(() => OnDisconnected(context.Request, connectionId).OrEmpty()); + }; + + return Transport.ProcessRequest(connection).OrEmpty().Catch(Counters.ErrorsAllTotal, Counters.ErrorsAllPerSec); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to catch any exception when unprotecting data.")] + internal string GetConnectionId(HostContext context, string connectionToken) + { + string unprotectedConnectionToken = null; + + try + { + unprotectedConnectionToken = ProtectedData.Unprotect(connectionToken, Purposes.ConnectionToken); + } + catch (Exception ex) + { + Trace.TraceInformation("Failed to process connectionToken {0}: {1}", connectionToken, ex); + } + + if (String.IsNullOrEmpty(unprotectedConnectionToken)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_ConnectionIdIncorrectFormat)); + } + + var tokens = unprotectedConnectionToken.Split(SplitChars, 2); + + string connectionId = tokens[0]; + string tokenUserName = tokens.Length > 1 ? tokens[1] : String.Empty; + string userName = GetUserIdentity(context); + + if (!String.Equals(tokenUserName, userName, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_UnrecognizedUserIdentity)); + } + + return connectionId; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to prevent any failures in unprotecting")] + internal IList VerifyGroups(HostContext context, string connectionId) + { + string groupsToken = context.Request.QueryString["groupsToken"]; + + if (String.IsNullOrEmpty(groupsToken)) + { + Trace.TraceInformation("The groups token is missing"); + + return ListHelper.Empty; + } + + string unprotectedGroupsToken = null; + + try + { + unprotectedGroupsToken = ProtectedData.Unprotect(groupsToken, Purposes.Groups); + } + catch (Exception ex) + { + Trace.TraceInformation("Failed to process groupsToken {0}: {1}", groupsToken, ex); + } + + if (String.IsNullOrEmpty(unprotectedGroupsToken)) + { + return ListHelper.Empty; + } + + var tokens = unprotectedGroupsToken.Split(SplitChars, 2); + + string groupConnectionId = tokens[0]; + string groupsValue = tokens.Length > 1 ? tokens[1] : String.Empty; + + if (!String.Equals(groupConnectionId, connectionId, StringComparison.OrdinalIgnoreCase)) + { + return ListHelper.Empty; + } + + return JsonSerializer.Parse(groupsValue); + } + + private IList AppendGroupPrefixes(HostContext context, string connectionId) + { + return (from g in OnRejoiningGroups(context.Request, VerifyGroups(context, connectionId), connectionId) + select GroupPrefix + g).ToList(); + } + + private Connection CreateConnection(string connectionId, IList signals, IList groups) + { + return new Connection(MessageBus, + JsonSerializer, + DefaultSignal, + connectionId, + signals, + groups, + TraceManager, + AckHandler, + Counters, + ProtectedData); + } + + /// + /// Returns the default signals for the . + /// + /// The id of the incoming connection. + /// The default signals for this . + private IList GetDefaultSignals(string connectionId) + { + // The list of default signals this connection cares about: + // 1. The default signal (the type name) + // 2. The connection id (so we can message this particular connection) + // 3. Ack signal + + return new string[] { + DefaultSignal, + PrefixHelper.GetConnectionId(connectionId), + PrefixHelper.GetAck(connectionId) + }; + } + + /// + /// Returns the signals used in the . + /// + /// The id of the incoming connection. + /// The signals used for this . + protected virtual IList GetSignals(string connectionId) + { + return GetDefaultSignals(connectionId); + } + + /// + /// Called before every request and gives the user a authorize the user. + /// + /// The for the current connection. + /// A boolean value that represents if the request is authorized. + protected virtual bool AuthorizeRequest(IRequest request) + { + return true; + } + + /// + /// Called when a connection reconnects after a timeout to determine which groups should be rejoined. + /// + /// The for the current connection. + /// The groups the calling connection claims to be part of. + /// The id of the reconnecting client. + /// A collection of group names that should be joined on reconnect + protected virtual IList OnRejoiningGroups(IRequest request, IList groups, string connectionId) + { + return groups; + } + + /// + /// Called when a new connection is made. + /// + /// The for the current connection. + /// The id of the connecting client. + /// A that completes when the connect operation is complete. + protected virtual Task OnConnected(IRequest request, string connectionId) + { + return TaskAsyncHelper.Empty; + } + + /// + /// Called when a connection reconnects after a timeout. + /// + /// The for the current connection. + /// The id of the re-connecting client. + /// A that completes when the re-connect operation is complete. + protected virtual Task OnReconnected(IRequest request, string connectionId) + { + return TaskAsyncHelper.Empty; + } + + /// + /// Called when data is received from a connection. + /// + /// The for the current connection. + /// The id of the connection sending the data. + /// The payload sent to the connection. + /// A that completes when the receive operation is complete. + protected virtual Task OnReceived(IRequest request, string connectionId, string data) + { + return TaskAsyncHelper.Empty; + } + + /// + /// Called when a connection disconnects. + /// + /// The for the current connection. + /// The id of the disconnected connection. + /// A that completes when the disconnect operation is complete. + protected virtual Task OnDisconnected(IRequest request, string connectionId) + { + return TaskAsyncHelper.Empty; + } + + private Task ProcessPingRequest(HostContext context) + { + var payload = new + { + Response = "pong" + }; + + if (!String.IsNullOrEmpty(context.Request.QueryString["callback"])) + { + return ProcessJsonpRequest(context, payload); + } + + context.Response.ContentType = JsonUtility.JsonMimeType; + return context.Response.End(JsonSerializer.Stringify(payload)); + } + + private Task ProcessNegotiationRequest(HostContext context) + { + // Total amount of time without a keep alive before the client should attempt to reconnect in seconds. + var keepAliveTimeout = _configurationManager.KeepAliveTimeout(); + string connectionId = Guid.NewGuid().ToString("d"); + string connectionToken = connectionId + ':' + GetUserIdentity(context); + + var payload = new + { + Url = context.Request.Url.LocalPath.Replace("/negotiate", ""), + ConnectionToken = ProtectedData.Protect(connectionToken, Purposes.ConnectionToken), + ConnectionId = connectionId, + KeepAliveTimeout = keepAliveTimeout != null ? keepAliveTimeout.Value.TotalSeconds : (double?)null, + DisconnectTimeout = _configurationManager.DisconnectTimeout.TotalSeconds, + TryWebSockets = _transportManager.SupportsTransport(WebSocketsTransportName) && context.SupportsWebSockets(), + WebSocketServerUrl = context.WebSocketServerUrl(), + ProtocolVersion = "1.2" + }; + + if (!String.IsNullOrEmpty(context.Request.QueryString["callback"])) + { + return ProcessJsonpRequest(context, payload); + } + + context.Response.ContentType = JsonUtility.JsonMimeType; + return context.Response.End(JsonSerializer.Stringify(payload)); + } + + private static string GetUserIdentity(HostContext context) + { + if (context.Request.User != null && context.Request.User.Identity.IsAuthenticated) + { + return context.Request.User.Identity.Name ?? String.Empty; + } + return String.Empty; + } + + private Task ProcessJsonpRequest(HostContext context, object payload) + { + context.Response.ContentType = JsonUtility.JavaScriptMimeType; + var data = JsonUtility.CreateJsonpCallback(context.Request.QueryString["callback"], JsonSerializer.Stringify(payload)); + + return context.Response.End(data); + } + + private static bool IsNegotiationRequest(IRequest request) + { + return request.Url.LocalPath.EndsWith("/negotiate", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsPingRequest(IRequest request) + { + return request.Url.LocalPath.EndsWith("/ping", StringComparison.OrdinalIgnoreCase); + } + + private ITransport GetTransport(HostContext context) + { + return _transportManager.GetTransport(context); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b73b3e766 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("Microsoft.AspNet.SignalR.Core")] +[assembly: AssemblyDescription("Async signaling library for .NET to help build real-time, multi-user interactive web applications.")] +#if SIGNED +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests.Common, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +#else +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.FunctionalTests")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.SignalR.Tests.Common")] +#endif diff --git a/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs b/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs new file mode 100644 index 000000000..312c37c5f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Resources.Designer.cs @@ -0,0 +1,378 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18010 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.SignalR { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.SignalR.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to /// <summary>Calls the {0} method on the server-side {1} hub.&#10;Returns a jQuery.Deferred() promise.</summary>. + /// + internal static string DynamicComment_CallsMethodOnServerSideDeferredPromise { + get { + return ResourceManager.GetString("DynamicComment_CallsMethodOnServerSideDeferredPromise", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /// <param name=\"{0}\" type=\"{1}\">Server side type is {2}</param>. + /// + internal static string DynamicComment_ServerSideTypeIs { + get { + return ResourceManager.GetString("DynamicComment_ServerSideTypeIs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Argument cannot be null or empty. + /// + internal static string Error_ArgumentNullOrEmpty { + get { + return ResourceManager.GetString("Error_ArgumentNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The buffer size '{0}' is out of range.. + /// + internal static string Error_BufferSizeOutOfRange { + get { + return ResourceManager.GetString("Error_BufferSizeOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Caller is not authorized to invoke the {0} method on {1}.. + /// + internal static string Error_CallerNotAuthorizedToInvokeMethodOn { + get { + return ResourceManager.GetString("Error_CallerNotAuthorizedToInvokeMethodOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The connection id is in the incorrect format.. + /// + internal static string Error_ConnectionIdIncorrectFormat { + get { + return ResourceManager.GetString("Error_ConnectionIdIncorrectFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The PersistentConnection is not initialized.. + /// + internal static string Error_ConnectionNotInitialized { + get { + return ResourceManager.GetString("Error_ConnectionNotInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DisconnectTimeout cannot be configured after the KeepAlive.. + /// + internal static string Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive { + get { + return ResourceManager.GetString("Error_DisconnectTimeoutCannotBeConfiguredAfterKeepAlive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DisconnectTimeout must be at least six seconds.. + /// + internal static string Error_DisconnectTimeoutMustBeAtLeastSixSeconds { + get { + return ResourceManager.GetString("Error_DisconnectTimeoutMustBeAtLeastSixSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not read RequireOutgoing. Use protected _requireOutgoing instead.. + /// + internal static string Error_DoNotReadRequireOutgoing { + get { + return ResourceManager.GetString("Error_DoNotReadRequireOutgoing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate hub names found.. + /// + internal static string Error_DuplicateHubs { + get { + return ResourceManager.GetString("Error_DuplicateHubs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate payload id detected for stream '{0}'.. + /// + internal static string Error_DuplicatePayloadsForStream { + get { + return ResourceManager.GetString("Error_DuplicatePayloadsForStream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error creating Hub {0}. . + /// + internal static string Error_ErrorCreatingHub { + get { + return ResourceManager.GetString("Error_ErrorCreatingHub", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' Hub could not be resolved.. + /// + internal static string Error_HubCouldNotBeResolved { + get { + return ResourceManager.GetString("Error_HubCouldNotBeResolved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There was an error invoking Hub method '{0}.{1}'.. + /// + internal static string Error_HubInvocationFailed { + get { + return ResourceManager.GetString("Error_HubInvocationFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid cursor.. + /// + internal static string Error_InvalidCursorFormat { + get { + return ResourceManager.GetString("Error_InvalidCursorFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The supplied frameId is in the incorrect format.. + /// + internal static string Error_InvalidForeverFrameId { + get { + return ResourceManager.GetString("Error_InvalidForeverFrameId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is not a {1}.. + /// + internal static string Error_IsNotA { + get { + return ResourceManager.GetString("Error_IsNotA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SignalR: JavaScript Hub proxy generation has been disabled.. + /// + internal static string Error_JavaScriptProxyDisabled { + get { + return ResourceManager.GetString("Error_JavaScriptProxyDisabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep Alive value must be greater than two seconds.. + /// + internal static string Error_KeepAliveMustBeGreaterThanTwoSeconds { + get { + return ResourceManager.GetString("Error_KeepAliveMustBeGreaterThanTwoSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep Alive value must be no more than a third of the DisconnectTimeout.. + /// + internal static string Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout { + get { + return ResourceManager.GetString("Error_KeepAliveMustBeNoMoreThanAThirdOfTheDisconnectTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' method could not be resolved.. + /// + internal static string Error_MethodCouldNotBeResolved { + get { + return ResourceManager.GetString("Error_MethodCouldNotBeResolved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Outgoing authorization can only be required for an entire Hub, not a specific method.. + /// + internal static string Error_MethodLevelOutgoingAuthorization { + get { + return ResourceManager.GetString("Error_MethodLevelOutgoingAuthorization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple activators for type {0} are registered. Please call GetServices instead.. + /// + internal static string Error_MultipleActivatorsAreaRegisteredCallGetServices { + get { + return ResourceManager.GetString("Error_MultipleActivatorsAreaRegisteredCallGetServices", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected end when reading object.. + /// + internal static string Error_ParseObjectFailed { + get { + return ResourceManager.GetString("Error_ParseObjectFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Protocol error: Missing connection token.. + /// + internal static string Error_ProtocolErrorMissingConnectionToken { + get { + return ResourceManager.GetString("Error_ProtocolErrorMissingConnectionToken", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Protocol error: Unknown transport.. + /// + internal static string Error_ProtocolErrorUnknownTransport { + get { + return ResourceManager.GetString("Error_ProtocolErrorUnknownTransport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to State has exceeded the maximum length of 4096 bytes.. + /// + internal static string Error_StateExceededMaximumLength { + get { + return ResourceManager.GetString("Error_StateExceededMaximumLength", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stream has been closed.. + /// + internal static string Error_StreamClosed { + get { + return ResourceManager.GetString("Error_StreamClosed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The stream is not open.. + /// + internal static string Error_StreamNotOpen { + get { + return ResourceManager.GetString("Error_StreamNotOpen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The queue is full.. + /// + internal static string Error_TaskQueueFull { + get { + return ResourceManager.GetString("Error_TaskQueueFull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to add module. The HubPipeline has already been invoked.. + /// + internal static string Error_UnableToAddModulePiplineAlreadyInvoked { + get { + return ResourceManager.GetString("Error_UnableToAddModulePiplineAlreadyInvoked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unrecognized user identity. The user identity cannot change during an active SignalR connection.. + /// + internal static string Error_UnrecognizedUserIdentity { + get { + return ResourceManager.GetString("Error_UnrecognizedUserIdentity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using a Hub instance not created by the HubPipeline is unsupported.. + /// + internal static string Error_UsingHubInstanceNotCreatedUnsupported { + get { + return ResourceManager.GetString("Error_UsingHubInstanceNotCreatedUnsupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WebSockets is not supported.. + /// + internal static string Error_WebSocketsNotSupported { + get { + return ResourceManager.GetString("Error_WebSocketsNotSupported", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Resources.resx b/src/Microsoft.AspNet.SignalR.Core/Resources.resx new file mode 100644 index 000000000..a858f4b40 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Resources.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + /// <summary>Calls the {0} method on the server-side {1} hub.&#10;Returns a jQuery.Deferred() promise.</summary> + + + /// <param name=\"{0}\" type=\"{1}\">Server side type is {2}</param> + + + Argument cannot be null or empty + + + The buffer size '{0}' is out of range. + + + Caller is not authorized to invoke the {0} method on {1}. + + + The connection id is in the incorrect format. + + + The PersistentConnection is not initialized. + + + DisconnectTimeout cannot be configured after the KeepAlive. + + + DisconnectTimeout must be at least six seconds. + + + Do not read RequireOutgoing. Use protected _requireOutgoing instead. + + + Duplicate hub names found. + + + Duplicate payload id detected for stream '{0}'. + + + Error creating Hub {0}. + + + '{0}' Hub could not be resolved. + + + There was an error invoking Hub method '{0}.{1}'. + + + Invalid cursor. + + + The supplied frameId is in the incorrect format. + + + '{0}' is not a {1}. + + + SignalR: JavaScript Hub proxy generation has been disabled. + + + Keep Alive value must be greater than two seconds. + + + Keep Alive value must be no more than a third of the DisconnectTimeout. + + + '{0}' method could not be resolved. + + + Outgoing authorization can only be required for an entire Hub, not a specific method. + + + Multiple activators for type {0} are registered. Please call GetServices instead. + + + Unexpected end when reading object. + + + Protocol error: Missing connection token. + + + Protocol error: Unknown transport. + + + State has exceeded the maximum length of 4096 bytes. + + + The stream has been closed. + + + The stream is not open. + + + The queue is full. + + + Unable to add module. The HubPipeline has already been invoked. + + + Unrecognized user identity. The user identity cannot change during an active SignalR connection. + + + Using a Hub instance not created by the HubPipeline is unsupported. + + + WebSockets is not supported. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js b/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js new file mode 100644 index 000000000..74b91d8f8 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Scripts/hubs.js @@ -0,0 +1,90 @@ +/*! + * ASP.NET SignalR JavaScript Library v1.1.3 + * http://signalr.net/ + * + * Copyright Microsoft Open Technologies, Inc. All rights reserved. + * Licensed under the Apache 2.0 + * https://github.com/SignalR/SignalR/blob/master/LICENSE.md + * + */ + +/// +/// +(function ($, window) { + /// + "use strict"; + + if (typeof ($.signalR) !== "function") { + throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/hubs."); + } + + var signalR = $.signalR; + + function makeProxyCallback(hub, callback) { + return function () { + // Call the client hub method + callback.apply(hub, $.makeArray(arguments)); + }; + } + + function registerHubProxies(instance, shouldSubscribe) { + var key, hub, memberKey, memberValue, subscriptionMethod; + + for (key in instance) { + if (instance.hasOwnProperty(key)) { + hub = instance[key]; + + if (!(hub.hubName)) { + // Not a client hub + continue; + } + + if (shouldSubscribe) { + // We want to subscribe to the hub events + subscriptionMethod = hub.on; + } + else { + // We want to unsubscribe from the hub events + subscriptionMethod = hub.off; + } + + // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe + for (memberKey in hub.client) { + if (hub.client.hasOwnProperty(memberKey)) { + memberValue = hub.client[memberKey]; + + if (!$.isFunction(memberValue)) { + // Not a client hub function + continue; + } + + subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue)); + } + } + } + } + } + + $.hubConnection.prototype.createHubProxies = function () { + var proxies = {}; + this.starting(function () { + // Register the hub proxies as subscribed + // (instance, shouldSubscribe) + registerHubProxies(proxies, true); + + this._registerSubscribedHubs(); + }).disconnected(function () { + // Unsubscribe all hub proxies when we "disconnect". This is to ensure that we do not re-add functional call backs. + // (instance, shouldSubscribe) + registerHubProxies(proxies, false); + }); + + /*hubs*/ + + return proxies; + }; + + signalR.hub = $.hubConnection("{serviceUrl}", { useDefaultPath: false }); + $.extend(signalR, signalR.hub.createHubProxies()); + +}(window.jQuery, window)); \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs b/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs new file mode 100644 index 000000000..56e25d106 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/TaskAsyncHelper.cs @@ -0,0 +1,1119 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR +{ + internal static class TaskAsyncHelper + { + private static readonly Task _emptyTask = MakeTask(null); + private static readonly Task _trueTask = MakeTask(true); + private static readonly Task _falseTask = MakeTask(false); + + private static Task MakeTask(T value) + { + return FromResult(value); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Empty + { + get + { + return _emptyTask; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task True + { + get + { + return _trueTask; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task False + { + get + { + return _falseTask; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task OrEmpty(this Task task) + { + return task ?? Empty; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task OrEmpty(this Task task) + { + return task ?? TaskCache.Empty; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromAsync(Func beginMethod, Action endMethod, object state) + { + try + { + return Task.Factory.FromAsync(beginMethod, endMethod, state); + } + catch (Exception ex) + { + return TaskAsyncHelper.FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromAsync(Func beginMethod, Func endMethod, object state) + { + try + { + return Task.Factory.FromAsync(beginMethod, endMethod, state); + } + catch (Exception ex) + { + return TaskAsyncHelper.FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Series(Func[] tasks, object[] state) + { + Task prev = TaskAsyncHelper.Empty; + Task finalTask = TaskAsyncHelper.Empty; + + for (int i = 0; i < tasks.Length; i++) + { + prev = finalTask; + finalTask = prev.Then(tasks[i], state[i]); + } + + return finalTask; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static TTask Catch(this TTask task) where TTask : Task + { + return Catch(task, ex => { }); + } + +#if PERFCOUNTERS + public static TTask Catch(this TTask task, params IPerformanceCounter[] counters) where TTask : Task + { + return Catch(task, _ => + { + if (counters == null) + { + return; + } + for (var i = 0; i < counters.Length; i++) + { + counters[i].Increment(); + } + }); + } +#endif + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static TTask Catch(this TTask task, Action handler, object state) where TTask : Task + { + if (task != null && task.Status != TaskStatus.RanToCompletion) + { + if (task.Status == TaskStatus.Faulted) + { + ExecuteOnFaulted(handler, state, task.Exception); + } + else + { + AttachFaultedContinuation(task, handler, state); + } + } + + return task; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + private static void AttachFaultedContinuation(TTask task, Action handler, object state) where TTask : Task + { + task.ContinueWith(innerTask => + { + ExecuteOnFaulted(handler, state, innerTask.Exception); + }, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + private static void ExecuteOnFaulted(Action handler, object state, AggregateException exception) + { + // observe Exception +#if !WINDOWS_PHONE && !SILVERLIGHT && !NETFX_CORE + Trace.TraceError("SignalR exception thrown by Task: {0}", exception); +#endif + handler(exception, state); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static TTask Catch(this TTask task, Action handler) where TTask : Task + { + return task.Catch((ex, state) => ((Action)state).Invoke(ex), + handler); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task ContinueWithNotComplete(this Task task, Action action) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + try + { + action(); + return task; + } + catch (Exception e) + { + return FromError(e); + } + case TaskStatus.RanToCompletion: + return task; + default: + var tcs = new TaskCompletionSource(); + + task.ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + try + { + action(); + + if (t.IsFaulted) + { + tcs.TrySetUnwrappedException(t.Exception); + } + else + { + tcs.TrySetCanceled(); + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + } + else + { + tcs.TrySetResult(null); + } + }, + TaskContinuationOptions.ExecuteSynchronously); + + return tcs.Task; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static void ContinueWithNotComplete(this Task task, TaskCompletionSource tcs) + { + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.SetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.SetCanceled(); + } + }, + TaskContinuationOptions.NotOnRanToCompletion); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static void ContinueWith(this Task task, TaskCompletionSource tcs) + { + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(null); + } + }, + TaskContinuationOptions.ExecuteSynchronously); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static void ContinueWith(this Task task, TaskCompletionSource tcs) + { + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(t.Result); + } + }); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Return(this Task[] tasks) + { + return Then(tasks, () => { }); + } + + // Then extesions + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Action successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor); + + default: + return RunTask(task, successor); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + return FromError(task.Exception); + + case TaskStatus.Canceled: + return Canceled(); + + case TaskStatus.RanToCompletion: + return FromMethod(successor); + + default: + return TaskRunners.RunTask(task, successor); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task[] tasks, Action successor) + { + if (tasks.Length == 0) + { + return FromMethod(successor); + } + + var tcs = new TaskCompletionSource(); + Task.Factory.ContinueWhenAll(tasks, completedTasks => + { + var faulted = completedTasks.FirstOrDefault(t => t.IsFaulted); + if (faulted != null) + { + tcs.SetUnwrappedException(faulted.Exception); + return; + } + var cancelled = completedTasks.FirstOrDefault(t => t.IsCanceled); + if (cancelled != null) + { + tcs.SetCanceled(); + return; + } + + successor(); + tcs.SetResult(null); + }); + + return tcs.Task; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Action successor, T1 arg1) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, arg1); + + default: + return GenericDelegates.ThenWithArgs(task, successor, arg1); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Action successor, T1 arg1, T2 arg2) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, arg1, arg2); + + default: + return GenericDelegates.ThenWithArgs(task, successor, arg1, arg2); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor, T1 arg1) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, arg1); + + default: + return GenericDelegates.ThenWithArgs(task, successor, arg1) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor, T1 arg1, T2 arg2) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, arg1, arg2); + + default: + return GenericDelegates.ThenWithArgs(task, successor, arg1, arg2) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func> successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + return FromError(task.Exception); + + case TaskStatus.Canceled: + return Canceled(); + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task.Result); + + default: + return TaskRunners>.RunTask(task, t => successor(t.Result)) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + return FromError(task.Exception); + + case TaskStatus.Canceled: + return Canceled(); + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task.Result); + + default: + return TaskRunners.RunTask(task, t => successor(t.Result)); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor, T1 arg1) + { + switch (task.Status) + { + case TaskStatus.Faulted: + return FromError(task.Exception); + + case TaskStatus.Canceled: + return Canceled(); + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task.Result, arg1); + + default: + return GenericDelegates.ThenWithArgs(task, successor, arg1); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor); + + default: + return TaskRunners.RunTask(task, successor) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func> successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + return FromError(task.Exception); + + case TaskStatus.Canceled: + return Canceled(); + + case TaskStatus.RanToCompletion: + return FromMethod(successor); + + default: + return TaskRunners>.RunTask(task, successor) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Action successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task.Result); + + default: + return TaskRunners.RunTask(task, successor); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task.Result); + + default: + return TaskRunners.RunTask(task, t => successor(t.Result)) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Then(this Task task, Func, T1, Task> successor, T1 arg1) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor, task, arg1); + + default: + return GenericDelegates, T1, object>.ThenWithArgs(task, successor, arg1) + .FastUnwrap(); + } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Finally(this Task task, Action next, object state) + { + try + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + next(state); + return task; + case TaskStatus.RanToCompletion: + return FromMethod(next, state); + + default: + return RunTaskSynchronously(task, next, state, onlyOnSuccess: false); + } + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task RunSynchronously(this Task task, Action successor) + { + switch (task.Status) + { + case TaskStatus.Faulted: + case TaskStatus.Canceled: + return task; + + case TaskStatus.RanToCompletion: + return FromMethod(successor); + + default: + return RunTaskSynchronously(task, state => ((Action)state).Invoke(), successor); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task FastUnwrap(this Task task) + { + var innerTask = (task.Status == TaskStatus.RanToCompletion) ? task.Result : null; + return innerTask ?? task.Unwrap(); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task FastUnwrap(this Task> task) + { + var innerTask = (task.Status == TaskStatus.RanToCompletion) ? task.Result : null; + return innerTask ?? task.Unwrap(); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task Delay(TimeSpan timeOut) + { +#if NETFX_CORE + return Task.Delay(timeOut); +#else + var tcs = new TaskCompletionSource(); + + var timer = new Timer(tcs.SetResult, + null, + timeOut, + TimeSpan.FromMilliseconds(-1)); + + return tcs.Task.ContinueWith(_ => + { + timer.Dispose(); + }, + TaskContinuationOptions.ExecuteSynchronously); +#endif + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Action func) + { + try + { + func(); + return Empty; + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Action func, T1 arg) + { + try + { + func(arg); + return Empty; + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Action func, T1 arg1, T2 arg2) + { + try + { + func(arg1, arg2); + return Empty; + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func) + { + try + { + return func(); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func> func) + { + try + { + return func(); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func) + { + try + { + return FromResult(func()); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func, T1 arg) + { + try + { + return func(arg); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func, T1 arg1, T2 arg2) + { + try + { + return func(arg1, arg2); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func> func, T1 arg) + { + try + { + return func(arg); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func, T1 arg) + { + try + { + return FromResult(func(arg)); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func> func, T1 arg1, T2 arg2) + { + try + { + return func(arg1, arg2); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + public static Task FromMethod(Func func, T1 arg1, T2 arg2) + { + try + { + return FromResult(func(arg1, arg2)); + } + catch (Exception ex) + { + return FromError(ex); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + public static Task FromResult(T value) + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(value); + return tcs.Task; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + internal static Task FromError(Exception e) + { + return FromError(e); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + internal static Task FromError(Exception e) + { + var tcs = new TaskCompletionSource(); + tcs.SetUnwrappedException(e); + return tcs.Task; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + internal static void SetUnwrappedException(this TaskCompletionSource tcs, Exception e) + { + var aggregateException = e as AggregateException; + if (aggregateException != null) + { + tcs.SetException(aggregateException.InnerExceptions); + } + else + { + tcs.SetException(e); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + internal static bool TrySetUnwrappedException(this TaskCompletionSource tcs, Exception e) + { + var aggregateException = e as AggregateException; + if (aggregateException != null) + { + return tcs.TrySetException(aggregateException.InnerExceptions); + } + else + { + return tcs.TrySetException(e); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + private static Task Canceled() + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + private static Task Canceled() + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + private static Task RunTask(Task task, Action successor) + { + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.SetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.SetCanceled(); + } + else + { + try + { + successor(); + tcs.SetResult(null); + } + catch (Exception ex) + { + tcs.SetUnwrappedException(ex); + } + } + }); + + return tcs.Task; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This is a shared file")] + private static Task RunTaskSynchronously(Task task, Action next, object state, bool onlyOnSuccess = true) + { + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => + { + try + { + if (t.IsFaulted) + { + if (!onlyOnSuccess) + { + next(state); + } + + tcs.SetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + if (!onlyOnSuccess) + { + next(state); + } + + tcs.SetCanceled(); + } + else + { + next(state); + tcs.SetResult(null); + } + } + catch (Exception ex) + { + tcs.SetUnwrappedException(ex); + } + }, + TaskContinuationOptions.ExecuteSynchronously); + + return tcs.Task; + } + + private static class TaskRunners + { + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + internal static Task RunTask(Task task, Action successor) + { + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.SetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.SetCanceled(); + } + else + { + try + { + successor(t.Result); + tcs.SetResult(null); + } + catch (Exception ex) + { + tcs.SetUnwrappedException(ex); + } + } + }); + + return tcs.Task; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + internal static Task RunTask(Task task, Func successor) + { + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.SetUnwrappedException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.SetCanceled(); + } + else + { + try + { + tcs.SetResult(successor()); + } + catch (Exception ex) + { + tcs.SetUnwrappedException(ex); + } + } + }); + + return tcs.Task; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are set in a tcs")] + internal static Task RunTask(Task task, Func, TResult> successor) + { + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => + { + if (task.IsFaulted) + { + tcs.SetUnwrappedException(t.Exception); + } + else if (task.IsCanceled) + { + tcs.SetCanceled(); + } + else + { + try + { + tcs.SetResult(successor(t)); + } + catch (Exception ex) + { + tcs.SetUnwrappedException(ex); + } + } + }); + + return tcs.Task; + } + } + + private static class GenericDelegates + { + internal static Task ThenWithArgs(Task task, Action successor, T1 arg1) + { + return RunTask(task, () => successor(arg1)); + } + + internal static Task ThenWithArgs(Task task, Action successor, T1 arg1, T2 arg2) + { + return RunTask(task, () => successor(arg1, arg2)); + } + + internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) + { + return TaskRunners.RunTask(task, () => successor(arg1)); + } + + internal static Task ThenWithArgs(Task task, Func successor, T1 arg1, T2 arg2) + { + return TaskRunners.RunTask(task, () => successor(arg1, arg2)); + } + + internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) + { + return TaskRunners.RunTask(task, t => successor(t.Result, arg1)); + } + + internal static Task ThenWithArgs(Task task, Func successor, T1 arg1) + { + return TaskRunners.RunTask(task, () => successor(arg1)); + } + + internal static Task ThenWithArgs(Task task, Func successor, T1 arg1, T2 arg2) + { + return TaskRunners.RunTask(task, () => successor(arg1, arg2)); + } + + internal static Task> ThenWithArgs(Task task, Func> successor, T1 arg1) + { + return TaskRunners>.RunTask(task, t => successor(t.Result, arg1)); + } + + internal static Task> ThenWithArgs(Task task, Func, T1, Task> successor, T1 arg1) + { + return TaskRunners>.RunTask(task, t => successor(t, arg1)); + } + } + + private static class TaskCache + { + public static Task Empty = MakeTask(default(T)); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs new file mode 100644 index 000000000..21b8f03dc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Tracing/ITraceManager.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Tracing +{ + public interface ITraceManager + { + SourceSwitch Switch { get; } + TraceSource this[string name] { get; } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs new file mode 100644 index 000000000..8ca4264a6 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceManager.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace Microsoft.AspNet.SignalR.Tracing +{ + public class TraceManager : ITraceManager + { + private readonly ConcurrentDictionary _sources = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + public TraceManager() + { + Switch = new SourceSwitch("SignalRSwitch"); + } + + public SourceSwitch Switch { get; private set; } + + public TraceSource this[string name] + { + get + { + return _sources.GetOrAdd(name, key => new TraceSource(key, SourceLevels.Off) + { + Switch = Switch + }); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs new file mode 100644 index 000000000..27a97c090 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Tracing/TraceSourceExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Diagnostics; + +namespace System.Diagnostics +{ + public static class TraceSourceExtensions + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] + public static void TraceVerbose(this TraceSource traceSource, string msg) + { + Trace(traceSource, TraceEventType.Verbose, msg); + } + + public static void TraceVerbose(this TraceSource traceSource, string format, params object[] args) + { + Trace(traceSource, TraceEventType.Verbose, format, args); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] + public static void TraceWarning(this TraceSource traceSource, string msg) + { + Trace(traceSource, TraceEventType.Warning, msg); + } + + public static void TraceWarning(this TraceSource traceSource, string format, params object[] args) + { + Trace(traceSource, TraceEventType.Warning, format, args); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "msg")] + public static void TraceError(this TraceSource traceSource, string msg) + { + Trace(traceSource, TraceEventType.Error, msg); + } + + public static void TraceError(this TraceSource traceSource, string format, params object[] args) + { + Trace(traceSource, TraceEventType.Error, format, args); + } + + private static void Trace(TraceSource traceSource, TraceEventType eventType, string msg) + { + traceSource.TraceEvent(eventType, 0, msg); + } + + private static void Trace(TraceSource traceSource, TraceEventType eventType, string format, params object[] args) + { + traceSource.TraceEvent(eventType, 0, format, args); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs new file mode 100644 index 000000000..9828d675b --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverFrameTransport.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Transports +{ + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")] + public class ForeverFrameTransport : ForeverTransport + { + private const string _initPrefix = "" + + "" + + "" + + "SignalR Forever Frame Transport Stream\r\n" + + "" + + "\r\n"; + + private HTMLTextWriter _htmlOutputWriter; + + public ForeverFrameTransport(HostContext context, IDependencyResolver resolver) + : base(context, resolver) + { + } + + /// + /// Pointed to the HTMLOutputWriter to wrap output stream with an HTML friendly one + /// + public override TextWriter OutputWriter + { + get + { + return HTMLOutputWriter; + } + } + + private HTMLTextWriter HTMLOutputWriter + { + get + { + if (_htmlOutputWriter == null) + { + _htmlOutputWriter = new HTMLTextWriter(Context.Response); + _htmlOutputWriter.NewLine = "\n"; + } + + return _htmlOutputWriter; + } + } + + public override Task KeepAlive() + { + if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted) + { + return TaskAsyncHelper.Empty; + } + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(state => PerformKeepAlive(state), this); + } + + public override Task Send(PersistentResponse response) + { + OnSendingResponse(response); + + var context = new ForeverFrameTransportContext(this, response); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(s => PerformSend(s), context); + } + + protected internal override Task InitializeResponse(ITransportConnection connection) + { + uint frameId; + string rawFrameId = Context.Request.QueryString["frameId"]; + if (String.IsNullOrWhiteSpace(rawFrameId) || !UInt32.TryParse(rawFrameId, NumberStyles.None, CultureInfo.InvariantCulture, out frameId)) + { + // Invalid frameId passed in + throw new InvalidOperationException(Resources.Error_InvalidForeverFrameId); + } + + string initScript = _initPrefix + + frameId.ToString(CultureInfo.InvariantCulture) + + _initSuffix; + + var context = new ForeverFrameTransportContext(this, initScript); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return base.InitializeResponse(connection).Then(s => Initialize(s), context); + } + + private static Task Initialize(object state) + { + var context = (ForeverFrameTransportContext)state; + + var initContext = new ForeverFrameTransportContext(context.Transport, context.State); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return WriteInit(initContext); + } + + private static Task WriteInit(ForeverFrameTransportContext context) + { + context.Transport.Context.Response.ContentType = "text/html; charset=UTF-8"; + + context.Transport.HTMLOutputWriter.WriteRaw((string)context.State); + context.Transport.HTMLOutputWriter.Flush(); + + return context.Transport.Context.Response.Flush(); + } + + private static Task PerformSend(object state) + { + var context = (ForeverFrameTransportContext)state; + + context.Transport.HTMLOutputWriter.WriteRaw("\r\n"); + context.Transport.HTMLOutputWriter.Flush(); + + return context.Transport.Context.Response.Flush(); + } + + private static Task PerformKeepAlive(object state) + { + var transport = (ForeverFrameTransport)state; + + transport.HTMLOutputWriter.WriteRaw(""); + transport.HTMLOutputWriter.WriteLine(); + transport.HTMLOutputWriter.WriteLine(); + transport.HTMLOutputWriter.Flush(); + + return transport.Context.Response.Flush(); + } + + private class ForeverFrameTransportContext + { + public ForeverFrameTransport Transport; + public object State; + + public ForeverFrameTransportContext(ForeverFrameTransport transport, object state) + { + Transport = transport; + State = state; + } + } + + private class HTMLTextWriter : BufferTextWriter + { + public HTMLTextWriter(IResponse response) + : base(response) + { + } + + public void WriteRaw(string value) + { + base.Write(value); + } + + public override void Write(string value) + { + base.Write(JavascriptEncode(value)); + } + + public override void WriteLine(string value) + { + base.WriteLine(JavascriptEncode(value)); + } + + private static string JavascriptEncode(string input) + { + return input.Replace("<", "\\u003c").Replace(">", "\\u003e"); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs new file mode 100644 index 000000000..8d3d9e9ed --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Transports +{ + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The disposer is an optimization")] + public abstract class ForeverTransport : TransportDisconnectBase, ITransport + { + private readonly IPerformanceCounterManager _counters; + private IJsonSerializer _jsonSerializer; + private string _lastMessageId; + + private const int MaxMessages = 10; + + protected ForeverTransport(HostContext context, IDependencyResolver resolver) + : this(context, + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve()) + { + } + + protected ForeverTransport(HostContext context, + IJsonSerializer jsonSerializer, + ITransportHeartbeat heartbeat, + IPerformanceCounterManager performanceCounterWriter, + ITraceManager traceManager) + : base(context, heartbeat, performanceCounterWriter, traceManager) + { + _jsonSerializer = jsonSerializer; + _counters = performanceCounterWriter; + } + + protected string LastMessageId + { + get + { + if (_lastMessageId == null) + { + _lastMessageId = Context.Request.QueryString["messageId"]; + } + + return _lastMessageId; + } + } + + protected IJsonSerializer JsonSerializer + { + get { return _jsonSerializer; } + } + + internal TaskCompletionSource InitializeTcs { get; set; } + + protected virtual void OnSending(string payload) + { + Heartbeat.MarkConnection(this); + } + + protected virtual void OnSendingResponse(PersistentResponse response) + { + Heartbeat.MarkConnection(this); + } + + public Func Received { get; set; } + + public Func TransportConnected { get; set; } + + public Func Connected { get; set; } + + public Func Reconnected { get; set; } + + // Unit testing hooks + internal Action AfterReceive; + internal Action BeforeCancellationTokenCallbackRegistered; + internal Action BeforeReceive; + internal Action AfterRequestEnd; + + protected override void InitializePersistentState() + { + // PersistentConnection.OnConnected must complete before we can write to the output stream, + // so clients don't indicate the connection has started too early. + InitializeTcs = new TaskCompletionSource(); + WriteQueue = new TaskQueue(InitializeTcs.Task); + + base.InitializePersistentState(); + } + + protected Task ProcessRequestCore(ITransportConnection connection) + { + Connection = connection; + + if (Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase)) + { + return ProcessSendRequest(); + } + else if (IsAbortRequest) + { + return Connection.Abort(ConnectionId); + } + else + { + InitializePersistentState(); + + return ProcessReceiveRequest(connection); + } + } + + public virtual Task ProcessRequest(ITransportConnection connection) + { + return ProcessRequestCore(connection); + } + + public abstract Task Send(PersistentResponse response); + + public virtual Task Send(object value) + { + var context = new ForeverTransportContext(this, value); + + return EnqueueOperation(state => PerformSend(state), context); + } + + protected internal virtual Task InitializeResponse(ITransportConnection connection) + { + return TaskAsyncHelper.Empty; + } + + protected internal override Task EnqueueOperation(Func writeAsync, object state) + { + Task task = base.EnqueueOperation(writeAsync, state); + + // If PersistentConnection.OnConnected has not completed (as indicated by InitializeTcs), + // the queue will be blocked to prevent clients from prematurely indicating the connection has + // started, but we must keep receive loop running to continue processing commands and to + // prevent deadlocks caused by waiting on ACKs. + if (InitializeTcs == null || InitializeTcs.Task.IsCompleted) + { + return task; + } + + return TaskAsyncHelper.Empty; + } + + private Task ProcessSendRequest() + { + string data = Context.Request.Form["data"]; + + if (Received != null) + { + return Received(data); + } + + return TaskAsyncHelper.Empty; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")] + private Task ProcessReceiveRequest(ITransportConnection connection) + { + Func initialize = null; + + bool newConnection = Heartbeat.AddConnection(this); + + if (IsConnectRequest) + { + if (newConnection) + { + initialize = Connected; + + _counters.ConnectionsConnected.Increment(); + } + } + else + { + initialize = Reconnected; + } + + var series = new Func[] + { + state => ((Func)state).Invoke(), + state => ((Func)state).Invoke() + }; + + var states = new object[] { TransportConnected ?? _emptyTaskFunc, + initialize ?? _emptyTaskFunc }; + + Func fullInit = () => TaskAsyncHelper.Series(series, states); + + return ProcessMessages(connection, fullInit); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The object is disposed otherwise")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")] + private Task ProcessMessages(ITransportConnection connection, Func initialize) + { + var disposer = new Disposer(); + + if (BeforeCancellationTokenCallbackRegistered != null) + { + BeforeCancellationTokenCallbackRegistered(); + } + + var cancelContext = new ForeverTransportContext(this, disposer); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext); + + var lifetime = new RequestLifetime(this, _requestLifeTime); + var messageContext = new MessageContext(this, lifetime, registration); + + if (BeforeReceive != null) + { + BeforeReceive(); + } + + try + { + // Ensure we enqueue the response initialization before any messages are received + EnqueueOperation(state => InitializeResponse((ITransportConnection)state), connection) + .Catch((ex, state) => OnError(ex, state), messageContext); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + IDisposable subscription = connection.Receive(LastMessageId, + (response, state) => OnMessageReceived(response, state), + MaxMessages, + messageContext); + + + disposer.Set(subscription); + + if (AfterReceive != null) + { + AfterReceive(); + } + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + initialize().Then(tcs => tcs.TrySetResult(null), InitializeTcs) + .Catch((ex, state) => OnError(ex, state), messageContext); + } + catch (OperationCanceledException ex) + { + InitializeTcs.TrySetCanceled(); + + lifetime.Complete(ex); + } + catch (Exception ex) + { + InitializeTcs.TrySetCanceled(); + + lifetime.Complete(ex); + } + + return _requestLifeTime.Task; + } + + private static void Cancel(object state) + { + var context = (ForeverTransportContext)state; + + context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")"); + + ((IDisposable)context.State).Dispose(); + } + + private static Task OnMessageReceived(PersistentResponse response, object state) + { + var context = (MessageContext)state; + + response.TimedOut = context.Transport.IsTimedOut; + + // If we're telling the client to disconnect then clean up the instantiated connection. + if (response.Disconnect) + { + // Send the response before removing any connection data + return context.Transport.Send(response).Then(c => OnDisconnectMessage(c), context) + .Then(() => TaskAsyncHelper.False); + } + else if (response.TimedOut || response.Aborted) + { + context.Registration.Dispose(); + + if (response.Aborted) + { + // If this was a clean disconnect raise the event. + return context.Transport.Abort() + .Then(() => TaskAsyncHelper.False); + } + } + + if (response.Terminal) + { + // End the request on the terminal response + context.Lifetime.Complete(); + + return TaskAsyncHelper.False; + } + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return context.Transport.Send(response) + .Then(() => TaskAsyncHelper.True); + } + + private static void OnDisconnectMessage(MessageContext context) + { + context.Transport.ApplyState(TransportConnectionStates.DisconnectMessageReceived); + + context.Registration.Dispose(); + + // Remove connection without triggering disconnect + context.Transport.Heartbeat.RemoveConnection(context.Transport); + } + + private static Task PerformSend(object state) + { + var context = (ForeverTransportContext)state; + + if (!context.Transport.IsAlive) + { + return TaskAsyncHelper.Empty; + } + + context.Transport.Context.Response.ContentType = JsonUtility.JsonMimeType; + + context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); + context.Transport.OutputWriter.Flush(); + + return context.Transport.Context.Response.End(); + } + + private static void OnError(AggregateException ex, object state) + { + var context = (MessageContext)state; + + context.Transport.IncrementErrors(); + + // Cancel any pending writes in the queue + context.Transport.InitializeTcs.TrySetCanceled(); + + // Complete the http request + context.Lifetime.Complete(ex); + } + + private class ForeverTransportContext + { + public object State; + public ForeverTransport Transport; + + public ForeverTransportContext(ForeverTransport foreverTransport, object state) + { + State = state; + Transport = foreverTransport; + } + } + + private class MessageContext + { + public ForeverTransport Transport; + public RequestLifetime Lifetime; + public IDisposable Registration; + + public MessageContext(ForeverTransport transport, RequestLifetime lifetime, IDisposable registration) + { + Registration = registration; + Lifetime = lifetime; + Transport = transport; + } + } + + private class RequestLifetime + { + private readonly HttpRequestLifeTime _lifetime; + private readonly ForeverTransport _transport; + + public RequestLifetime(ForeverTransport transport, HttpRequestLifeTime lifetime) + { + _lifetime = lifetime; + _transport = transport; + } + + public void Complete() + { + Complete(error: null); + } + + public void Complete(Exception error) + { + _lifetime.Complete(error); + + _transport.Dispose(); + + if (_transport.AfterRequestEnd != null) + { + _transport.AfterRequestEnd(error); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs new file mode 100644 index 000000000..1354dcb2c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/HttpRequestLifeTime.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Transports +{ + internal class HttpRequestLifeTime + { + private readonly TaskCompletionSource _lifetimeTcs = new TaskCompletionSource(); + private readonly TransportDisconnectBase _transport; + private readonly TaskQueue _writeQueue; + private readonly TraceSource _trace; + private readonly string _connectionId; + + public HttpRequestLifeTime(TransportDisconnectBase transport, TaskQueue writeQueue, TraceSource trace, string connectionId) + { + _transport = transport; + _trace = trace; + _connectionId = connectionId; + _writeQueue = writeQueue; + } + + public Task Task + { + get + { + return _lifetimeTcs.Task; + } + } + + public void Complete() + { + Complete(error: null); + } + + public void Complete(Exception error) + { + _trace.TraceEvent(TraceEventType.Verbose, 0, "DrainWrites(" + _connectionId + ")"); + + var context = new LifetimeContext(_transport, _lifetimeTcs, error); + + _transport.ApplyState(TransportConnectionStates.QueueDrained); + + // Drain the task queue for pending write operations so we don't end the request and then try to write + // to a corrupted request object. + _writeQueue.Drain().Catch().Finally(state => + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + ((LifetimeContext)state).Complete(); + }, + context); + + if (error != null) + { + _trace.TraceEvent(TraceEventType.Error, 0, "CompleteRequest (" + _connectionId + ") failed: " + error.GetBaseException()); + } + else + { + _trace.TraceInformation("CompleteRequest (" + _connectionId + ")"); + } + } + + private class LifetimeContext + { + private readonly TaskCompletionSource _lifetimeTcs; + private readonly Exception _error; + private readonly TransportDisconnectBase _transport; + + public LifetimeContext(TransportDisconnectBase transport, TaskCompletionSource lifeTimetcs, Exception error) + { + _transport = transport; + _lifetimeTcs = lifeTimetcs; + _error = error; + } + + public void Complete() + { + _transport.ApplyState(TransportConnectionStates.HttpRequestEnded); + + if (_error != null) + { + _lifetimeTcs.TrySetUnwrappedException(_error); + } + else + { + _lifetimeTcs.TrySetResult(null); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs new file mode 100644 index 000000000..452e88def --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITrackingConnection.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Represents a connection that can be tracked by an . + /// + public interface ITrackingConnection : IDisposable + { + /// + /// Gets the id of the connection. + /// + string ConnectionId { get; } + + /// + /// Gets a cancellation token that represents the connection's lifetime. + /// + CancellationToken CancellationToken { get; } + + /// + /// Gets a value that represents if the connection is alive. + /// + bool IsAlive { get; } + + /// + /// Gets a value that represents if the connection is timed out. + /// + bool IsTimedOut { get; } + + /// + /// Gets a value that represents if the connection supprots keep alive. + /// + bool SupportsKeepAlive { get; } + + /// + /// Gets a value indicating the amount of time to wait after the connection dies before firing the disconnecting the connection. + /// + TimeSpan DisconnectThreshold { get; } + + /// + /// Gets the uri of the connection. + /// + Uri Url { get; } + + /// + /// Applies a new state to the connection. + /// + void ApplyState(TransportConnectionStates states); + + /// + /// Causes the connection to disconnect. + /// + Task Disconnect(); + + /// + /// Causes the connection to timeout. + /// + void Timeout(); + + /// + /// Sends a keep alive ping over the connection. + /// + Task KeepAlive(); + + /// + /// Kills the connection. + /// + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "End", Justification = "Ends the connction thus the name is appropriate.")] + void End(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs new file mode 100644 index 000000000..1bf870bcc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Represents a transport that communicates + /// + public interface ITransport + { + /// + /// Gets or sets a callback that is invoked when the transport receives data. + /// + Func Received { get; set; } + + /// + /// Gets or sets a callback that is invoked when the initial connection connects to the transport. + /// + Func Connected { get; set; } + + /// + /// Gets or sets a callback that is invoked when the transport connects. + /// + Func TransportConnected { get; set; } + + /// + /// Gets or sets a callback that is invoked when the transport reconnects. + /// + Func Reconnected { get; set; } + + /// + /// Gets or sets a callback that is invoked when the transport disconnects. + /// + Func Disconnected { get; set; } + + /// + /// Gets or sets the connection id for the transport. + /// + string ConnectionId { get; set; } + + /// + /// Processes the specified for this transport. + /// + /// The to process. + /// A that completes when the transport has finished processing the connection. + Task ProcessRequest(ITransportConnection connection); + + /// + /// Sends data over the transport. + /// + /// The value to be sent. + /// A that completes when the send is complete. + Task Send(object value); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs new file mode 100644 index 000000000..ee41c6c1e --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportConnection.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.Transports +{ + public interface ITransportConnection + { + IDisposable Receive(string messageId, Func> callback, int maxMessages, object state); + + Task Send(ConnectionMessage message); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs new file mode 100644 index 000000000..17a24e825 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportHeartBeat.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Manages tracking the state of connections. + /// + public interface ITransportHeartbeat + { + /// + /// Adds a new connection to the list of tracked connections. + /// + /// The connection to be added. + bool AddConnection(ITrackingConnection connection); + + /// + /// Marks an existing connection as active. + /// + /// The connection to mark. + void MarkConnection(ITrackingConnection connection); + + /// + /// Removes a connection from the list of tracked connections. + /// + /// The connection to remove. + void RemoveConnection(ITrackingConnection connection); + + /// + /// Gets a list of connections being tracked. + /// + /// A list of connections. + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive.")] + IList GetConnections(); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs new file mode 100644 index 000000000..630cb4639 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransportManager.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using Microsoft.AspNet.SignalR.Hosting; +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Manages the transports for connections. + /// + public interface ITransportManager + { + /// + /// Gets the specified transport for the specified . + /// + /// The for the current request. + /// The for the specified . + ITransport GetTransport(HostContext hostContext); + + /// + /// Determines whether the specified transport is supported. + /// + /// The name of the transport to test. + /// True if the transport is supported, otherwise False. + bool SupportsTransport(string transportName); + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs new file mode 100644 index 000000000..2c645c794 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs @@ -0,0 +1,401 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Transports +{ + public class LongPollingTransport : TransportDisconnectBase, ITransport + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IPerformanceCounterManager _counters; + + // This should be ok to do since long polling request never hang around too long + // so we won't bloat memory + private const int MaxMessages = 5000; + + public LongPollingTransport(HostContext context, IDependencyResolver resolver) + : this(context, + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve()) + { + + } + + public LongPollingTransport(HostContext context, + IJsonSerializer jsonSerializer, + ITransportHeartbeat heartbeat, + IPerformanceCounterManager performanceCounterManager, + ITraceManager traceManager) + : base(context, heartbeat, performanceCounterManager, traceManager) + { + _jsonSerializer = jsonSerializer; + _counters = performanceCounterManager; + } + + /// + /// The number of milliseconds to tell the browser to wait before restablishing a + /// long poll connection after data is sent from the server. Defaults to 0. + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "long", Justification = "Longpolling is a well known term")] + public static long LongPollDelay + { + get; + set; + } + + public override TimeSpan DisconnectThreshold + { + get { return TimeSpan.FromMilliseconds(LongPollDelay); } + } + + public override bool IsConnectRequest + { + get + { + return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase); + } + } + + private bool IsReconnectRequest + { + get + { + return Context.Request.Url.LocalPath.EndsWith("/reconnect", StringComparison.OrdinalIgnoreCase); + } + } + + private bool IsJsonp + { + get + { + return !String.IsNullOrEmpty(JsonpCallback); + } + } + + private bool IsSendRequest + { + get + { + return Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase); + } + } + + private string MessageId + { + get + { + return Context.Request.QueryString["messageId"]; + } + } + + private string JsonpCallback + { + get + { + return Context.Request.QueryString["callback"]; + } + } + + public override bool SupportsKeepAlive + { + get + { + return false; + } + } + + public Func Received { get; set; } + + public Func TransportConnected { get; set; } + + public Func Connected { get; set; } + + public Func Reconnected { get; set; } + + public Task ProcessRequest(ITransportConnection connection) + { + Connection = connection; + + if (IsSendRequest) + { + return ProcessSendRequest(); + } + else if (IsAbortRequest) + { + return Connection.Abort(ConnectionId); + } + else + { + InitializePersistentState(); + + return ProcessReceiveRequest(connection); + } + } + + public Task Send(PersistentResponse response) + { + Heartbeat.MarkConnection(this); + + AddTransportData(response); + + return Send((object)response); + } + + public Task Send(object value) + { + var context = new LongPollingTransportContext(this, value); + + return EnqueueOperation(state => PerformSend(state), context); + } + + private Task ProcessSendRequest() + { + string data = Context.Request.Form["data"] ?? Context.Request.QueryString["data"]; + + if (Received != null) + { + return Received(data); + } + + return TaskAsyncHelper.Empty; + } + + private Task ProcessReceiveRequest(ITransportConnection connection) + { + Func initialize = null; + + bool newConnection = Heartbeat.AddConnection(this); + + if (IsConnectRequest) + { + if (newConnection) + { + initialize = Connected; + + _counters.ConnectionsConnected.Increment(); + } + } + else if (IsReconnectRequest) + { + initialize = Reconnected; + } + + var series = new Func[] + { + state => ((Func)state).Invoke(), + state => ((Func)state).Invoke() + }; + + var states = new object[] { TransportConnected ?? _emptyTaskFunc, + initialize ?? _emptyTaskFunc }; + + Func fullInit = () => TaskAsyncHelper.Series(series, states); + + return ProcessMessages(connection, fullInit); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The subscription is disposed in the callback")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is captured in a task")] + private Task ProcessMessages(ITransportConnection connection, Func initialize) + { + var disposer = new Disposer(); + + var cancelContext = new LongPollingTransportContext(this, disposer); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext); + + var lifeTime = new RequestLifetime(this, _requestLifeTime, registration); + var messageContext = new MessageContext(this, lifeTime); + + try + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + IDisposable subscription = connection.Receive(MessageId, + (response, state) => OnMessageReceived(response, state), + MaxMessages, + messageContext); + + // Set the disposable + disposer.Set(subscription); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + initialize().Catch((ex, state) => OnError(ex, state), messageContext); + } + catch (Exception ex) + { + lifeTime.Complete(ex); + } + + return _requestLifeTime.Task; + } + + private static void Cancel(object state) + { + var context = (LongPollingTransportContext)state; + + context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")"); + + ((IDisposable)context.State).Dispose(); + } + + private static Task OnMessageReceived(PersistentResponse response, object state) + { + var context = (MessageContext)state; + + response.TimedOut = context.Transport.IsTimedOut; + + Task task = TaskAsyncHelper.Empty; + + if (response.Aborted) + { + // If this was a clean disconnect then raise the event + task = context.Transport.Abort(); + } + + if (response.Terminal) + { + // If the response wasn't sent, send it before ending the request + if (!context.ResponseSent) + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response) + .Then(() => + { + context.Lifetime.Complete(); + + return TaskAsyncHelper.False; + }); + } + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return task.Then(() => + { + context.Lifetime.Complete(); + + return TaskAsyncHelper.False; + }); + } + + // Mark the response as sent + context.ResponseSent = true; + + // Send the response and return false + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response) + .Then(() => TaskAsyncHelper.False); + } + + private static Task PerformSend(object state) + { + var context = (LongPollingTransportContext)state; + + if (!context.Transport.IsAlive) + { + return TaskAsyncHelper.Empty; + } + + context.Transport.Context.Response.ContentType = context.Transport.IsJsonp ? JsonUtility.JavaScriptMimeType : JsonUtility.JsonMimeType; + + if (context.Transport.IsJsonp) + { + context.Transport.OutputWriter.Write(context.Transport.JsonpCallback); + context.Transport.OutputWriter.Write("("); + } + + context.Transport._jsonSerializer.Serialize(context.State, context.Transport.OutputWriter); + + if (context.Transport.IsJsonp) + { + context.Transport.OutputWriter.Write(");"); + } + + context.Transport.OutputWriter.Flush(); + + return context.Transport.Context.Response.End(); + } + + private static void OnError(AggregateException ex, object state) + { + var context = (MessageContext)state; + + context.Transport.IncrementErrors(); + + context.Lifetime.Complete(ex); + } + + private static void AddTransportData(PersistentResponse response) + { + if (LongPollDelay > 0) + { + response.LongPollDelay = LongPollDelay; + } + } + + private class LongPollingTransportContext + { + public object State; + public LongPollingTransport Transport; + + public LongPollingTransportContext(LongPollingTransport transport, object state) + { + State = state; + Transport = transport; + } + } + + private class MessageContext + { + public LongPollingTransport Transport; + public RequestLifetime Lifetime; + public bool ResponseSent; + + public MessageContext(LongPollingTransport longPollingTransport, RequestLifetime requestLifetime) + { + Transport = longPollingTransport; + Lifetime = requestLifetime; + } + } + + private class RequestLifetime + { + private readonly HttpRequestLifeTime _requestLifeTime; + private readonly LongPollingTransport _transport; + private readonly IDisposable _registration; + + public RequestLifetime(LongPollingTransport transport, HttpRequestLifeTime requestLifeTime, IDisposable registration) + { + _transport = transport; + _registration = registration; + _requestLifeTime = requestLifeTime; + } + + public void Complete() + { + Complete(exception: null); + } + + public void Complete(Exception exception) + { + // End the request + _requestLifeTime.Complete(exception); + + // Dispose of the cancellation token subscription + _registration.Dispose(); + + // Dispose any state on the transport + _transport.Dispose(); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs new file mode 100644 index 000000000..17c1bd2d1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/PersistentResponse.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Messaging; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Represents a response to a connection. + /// + public sealed class PersistentResponse : IJsonWritable + { + private readonly Func _exclude; + private readonly Action _writeCursor; + + public PersistentResponse() + : this(message => true, writer => { }) + { + + } + + /// + /// Creates a new instance of . + /// + /// A filter that determines whether messages should be written to the client. + /// The cursor writer. + public PersistentResponse(Func exclude, Action writeCursor) + { + _exclude = exclude; + _writeCursor = writeCursor; + } + + /// + /// The list of messages to be sent to the receiving connection. + /// + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization and this type is only used for serialization.")] + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is only used for serialization")] + public IList> Messages { get; set; } + + public bool Terminal { get; set; } + + /// + /// The total count of the messages sent the receiving connection. + /// + public int TotalCount { get; set; } + + /// + /// True if the connection receives a disconnect command. + /// + public bool Disconnect { get; set; } + + /// + /// True if the connection was forcibly closed. + /// + public bool Aborted { get; set; } + + /// + /// True if the connection timed out. + /// + public bool TimedOut { get; set; } + + /// + /// Signed token representing the list of groups. Updates on change. + /// + public string GroupsToken { get; set; } + + /// + /// The time the long polling client should wait before reestablishing a connection if no data is received. + /// + public long? LongPollDelay { get; set; } + + /// + /// Serializes only the necessary components of the to JSON + /// using Json.NET's JsonTextWriter to improve performance. + /// + /// The that receives the JSON serialization. + void IJsonWritable.WriteJson(TextWriter writer) + { + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + var jsonWriter = new JsonTextWriter(writer); + jsonWriter.WriteStartObject(); + + // REVIEW: Is this 100% correct? + writer.Write('"'); + writer.Write("C"); + writer.Write('"'); + writer.Write(':'); + writer.Write('"'); + _writeCursor(writer); + writer.Write('"'); + writer.Write(','); + + if (Disconnect) + { + jsonWriter.WritePropertyName("D"); + jsonWriter.WriteValue(1); + } + + if (TimedOut) + { + jsonWriter.WritePropertyName("T"); + jsonWriter.WriteValue(1); + } + + if (GroupsToken != null) + { + jsonWriter.WritePropertyName("G"); + jsonWriter.WriteValue(GroupsToken); + } + + if (LongPollDelay.HasValue) + { + jsonWriter.WritePropertyName("L"); + jsonWriter.WriteValue(LongPollDelay.Value); + } + + jsonWriter.WritePropertyName("M"); + jsonWriter.WriteStartArray(); + + WriteMessages(writer, jsonWriter); + + jsonWriter.WriteEndArray(); + jsonWriter.WriteEndObject(); + } + + private void WriteMessages(TextWriter writer, JsonTextWriter jsonWriter) + { + if (Messages == null) + { + return; + } + + // If the writer is a binary writer then write to the underlying writer directly + var binaryWriter = writer as IBinaryWriter; + + bool first = true; + + for (int i = 0; i < Messages.Count; i++) + { + ArraySegment segment = Messages[i]; + for (int j = segment.Offset; j < segment.Offset + segment.Count; j++) + { + Message message = segment.Array[j]; + + if (!message.IsCommand && !_exclude(message)) + { + if (binaryWriter != null) + { + if (!first) + { + // We need to write the array separator manually + writer.Write(','); + } + + // If we can write binary then just write it + binaryWriter.Write(message.Value); + + first = false; + } + else + { + // Write the raw JSON value + jsonWriter.WriteRawValue(message.GetString()); + } + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs new file mode 100644 index 000000000..ef22be398 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ServerSentEventsTransport.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; + +namespace Microsoft.AspNet.SignalR.Transports +{ + public class ServerSentEventsTransport : ForeverTransport + { + public ServerSentEventsTransport(HostContext context, IDependencyResolver resolver) + : base(context, resolver) + { + } + + public override Task KeepAlive() + { + if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted) + { + return TaskAsyncHelper.Empty; + } + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(state => PerformKeepAlive(state), this); + } + + public override Task Send(PersistentResponse response) + { + OnSendingResponse(response); + + var context = new SendContext(this, response); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(state => PerformSend(state), context); + } + + protected internal override Task InitializeResponse(ITransportConnection connection) + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return base.InitializeResponse(connection) + .Then(s => WriteInit(s), this); + } + + private static Task PerformKeepAlive(object state) + { + var transport = (ServerSentEventsTransport)state; + + transport.OutputWriter.Write("data: {}"); + transport.OutputWriter.WriteLine(); + transport.OutputWriter.WriteLine(); + transport.OutputWriter.Flush(); + + return transport.Context.Response.Flush(); + } + + private static Task PerformSend(object state) + { + var context = (SendContext)state; + + context.Transport.OutputWriter.Write("data: "); + context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); + context.Transport.OutputWriter.WriteLine(); + context.Transport.OutputWriter.WriteLine(); + context.Transport.OutputWriter.Flush(); + + return context.Transport.Context.Response.Flush(); + } + + private static Task WriteInit(ServerSentEventsTransport transport) + { + transport.Context.Response.ContentType = "text/event-stream"; + + // "data: initialized\n\n" + transport.OutputWriter.Write("data: initialized"); + transport.OutputWriter.WriteLine(); + transport.OutputWriter.WriteLine(); + transport.OutputWriter.Flush(); + + return transport.Context.Response.Flush(); + } + + private class SendContext + { + public ServerSentEventsTransport Transport; + public object State; + + public SendContext(ServerSentEventsTransport transport, object state) + { + Transport = transport; + State = state; + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs new file mode 100644 index 000000000..1f12d533f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Messaging; + +namespace Microsoft.AspNet.SignalR.Transports +{ + internal static class TransportConnectionExtensions + { + internal static Task Close(this ITransportConnection connection, string connectionId) + { + return SendCommand(connection, connectionId, CommandType.Disconnect); + } + + internal static Task Abort(this ITransportConnection connection, string connectionId) + { + return SendCommand(connection, connectionId, CommandType.Abort); + } + + private static Task SendCommand(ITransportConnection connection, string connectionId, CommandType commandType) + { + var command = new Command + { + CommandType = commandType + }; + + var message = new ConnectionMessage(PrefixHelper.GetConnectionId(connectionId), + command); + + return connection.Send(message); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs new file mode 100644 index 000000000..6ffddb4e5 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportConnectionStates.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.AspNet.SignalR.Transports +{ + [Flags] + public enum TransportConnectionStates + { + None = 0, + Added = 1, + Removed = 2, + Replaced = 4, + QueueDrained = 8, + HttpRequestEnded = 16, + Disconnected = 32, + Aborted = 64, + DisconnectMessageReceived = 128, + Disposed = 65536, + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs new file mode 100644 index 000000000..4b2c939c7 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Transports +{ + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")] + public abstract class TransportDisconnectBase : ITrackingConnection + { + private readonly HostContext _context; + private readonly ITransportHeartbeat _heartbeat; + private TextWriter _outputWriter; + + private TraceSource _trace; + + private int _timedOut; + private readonly IPerformanceCounterManager _counters; + private int _ended; + private TransportConnectionStates _state; + + internal static readonly Func _emptyTaskFunc = () => TaskAsyncHelper.Empty; + + // Token that represents the end of the connection based on a combination of + // conditions (timeout, disconnect, connection forcibly ended, host shutdown) + private CancellationToken _connectionEndToken; + private SafeCancellationTokenSource _connectionEndTokenSource; + + // Token that represents the host shutting down + private CancellationToken _hostShutdownToken; + private IDisposable _hostRegistration; + private IDisposable _connectionEndRegistration; + + internal HttpRequestLifeTime _requestLifeTime; + + protected TransportDisconnectBase(HostContext context, ITransportHeartbeat heartbeat, IPerformanceCounterManager performanceCounterManager, ITraceManager traceManager) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (heartbeat == null) + { + throw new ArgumentNullException("heartbeat"); + } + + if (performanceCounterManager == null) + { + throw new ArgumentNullException("performanceCounterManager"); + } + + if (traceManager == null) + { + throw new ArgumentNullException("traceManager"); + } + + _context = context; + _heartbeat = heartbeat; + _counters = performanceCounterManager; + + // Queue to protect against overlapping writes to the underlying response stream + WriteQueue = new TaskQueue(); + + _trace = traceManager["SignalR.Transports." + GetType().Name]; + } + + protected TraceSource Trace + { + get + { + return _trace; + } + } + + public string ConnectionId + { + get; + set; + } + + public virtual TextWriter OutputWriter + { + get + { + if (_outputWriter == null) + { + _outputWriter = CreateResponseWriter(); + _outputWriter.NewLine = "\n"; + } + + return _outputWriter; + } + } + + internal TaskQueue WriteQueue + { + get; + set; + } + + public Func Disconnected { get; set; } + + public virtual CancellationToken CancellationToken + { + get { return _context.Response.CancellationToken; } + } + + public virtual bool IsAlive + { + get + { + // If the CTS is tripped or the request has ended then the connection isn't alive + return !(CancellationToken.IsCancellationRequested || (_requestLifeTime != null && _requestLifeTime.Task.IsCompleted)); + } + } + + protected CancellationToken ConnectionEndToken + { + get + { + return _connectionEndToken; + } + } + + public bool IsTimedOut + { + get + { + return _timedOut == 1; + } + } + + public virtual bool SupportsKeepAlive + { + get + { + return true; + } + } + + public virtual TimeSpan DisconnectThreshold + { + get { return TimeSpan.FromSeconds(5); } + } + + public virtual bool IsConnectRequest + { + get + { + return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase); + } + } + + protected bool IsAbortRequest + { + get + { + return Context.Request.Url.LocalPath.EndsWith("/abort", StringComparison.OrdinalIgnoreCase); + } + } + + protected ITransportConnection Connection { get; set; } + + protected HostContext Context + { + get { return _context; } + } + + protected ITransportHeartbeat Heartbeat + { + get { return _heartbeat; } + } + + public Uri Url + { + get { return _context.Request.Url; } + } + + protected virtual TextWriter CreateResponseWriter() + { + return new BufferTextWriter(Context.Response); + } + + protected void IncrementErrors() + { + _counters.ErrorsTransportTotal.Increment(); + _counters.ErrorsTransportPerSec.Increment(); + _counters.ErrorsAllTotal.Increment(); + _counters.ErrorsAllPerSec.Increment(); + } + + public Task Disconnect() + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return Abort(clean: false).Then(transport => transport.Connection.Close(transport.ConnectionId), this); + } + + public Task Abort() + { + return Abort(clean: true); + } + + public Task Abort(bool clean) + { + if (clean) + { + ApplyState(TransportConnectionStates.Aborted); + } + else + { + ApplyState(TransportConnectionStates.Disconnected); + } + + Trace.TraceInformation("Abort(" + ConnectionId + ")"); + + // When a connection is aborted (graceful disconnect) we send a command to it + // telling to to disconnect. At that moment, we raise the disconnect event and + // remove this connection from the heartbeat so we don't end up raising it for the same connection. + Heartbeat.RemoveConnection(this); + + // End the connection + End(); + + var disconnected = Disconnected ?? _emptyTaskFunc; + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return disconnected().Catch((ex, state) => OnDisconnectError(ex, state), Trace) + .Then(counters => counters.ConnectionsDisconnected.Increment(), _counters); + } + + public void ApplyState(TransportConnectionStates states) + { + _state |= states; + } + + public void Timeout() + { + if (Interlocked.Exchange(ref _timedOut, 1) == 0) + { + Trace.TraceInformation("Timeout(" + ConnectionId + ")"); + + End(); + } + } + + public virtual Task KeepAlive() + { + return TaskAsyncHelper.Empty; + } + + public void End() + { + if (Interlocked.Exchange(ref _ended, 1) == 0) + { + Trace.TraceInformation("End(" + ConnectionId + ")"); + + if (_connectionEndTokenSource != null) + { + _connectionEndTokenSource.Cancel(); + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _connectionEndTokenSource.Dispose(); + _connectionEndRegistration.Dispose(); + _hostRegistration.Dispose(); + + ApplyState(TransportConnectionStates.Disposed); + } + } + + protected virtual internal Task EnqueueOperation(Func writeAsync) + { + return EnqueueOperation(state => ((Func)state).Invoke(), writeAsync); + } + + protected virtual internal Task EnqueueOperation(Func writeAsync, object state) + { + if (!IsAlive) + { + return TaskAsyncHelper.Empty; + } + + // Only enqueue new writes if the connection is alive + return WriteQueue.Enqueue(writeAsync, state); + } + + protected virtual void InitializePersistentState() + { + _hostShutdownToken = _context.HostShutdownToken(); + + _requestLifeTime = new HttpRequestLifeTime(this, WriteQueue, Trace, ConnectionId); + + // Create a token that represents the end of this connection's life + _connectionEndTokenSource = new SafeCancellationTokenSource(); + _connectionEndToken = _connectionEndTokenSource.Token; + + // Handle the shutdown token's callback so we can end our token if it trips + _hostRegistration = _hostShutdownToken.SafeRegister(state => + { + ((SafeCancellationTokenSource)state).Cancel(); + }, + _connectionEndTokenSource); + + // When the connection ends release the request + _connectionEndRegistration = CancellationToken.SafeRegister(state => + { + ((HttpRequestLifeTime)state).Complete(); + }, + _requestLifeTime); + } + + private static void OnDisconnectError(AggregateException ex, object state) + { + ((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to raise disconnect: " + ex.GetBaseException()); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs new file mode 100644 index 000000000..f44bb8216 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportHeartBeat.cs @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.SignalR.Configuration; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// Default implementation of . + /// + public class TransportHeartbeat : ITransportHeartbeat, IDisposable + { + private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); + private readonly Timer _timer; + private readonly IConfigurationManager _configurationManager; + private readonly IServerCommandHandler _serverCommandHandler; + private readonly TraceSource _trace; + private readonly string _serverId; + private readonly IPerformanceCounterManager _counters; + private readonly object _counterLock = new object(); + + private int _running; + private ulong _heartbeatCount; + + /// + /// Initializes and instance of the class. + /// + /// The . + public TransportHeartbeat(IDependencyResolver resolver) + { + _configurationManager = resolver.Resolve(); + _serverCommandHandler = resolver.Resolve(); + _serverId = resolver.Resolve().ServerId; + _counters = resolver.Resolve(); + + var traceManager = resolver.Resolve(); + _trace = traceManager["SignalR.Transports.TransportHeartBeat"]; + + _serverCommandHandler.Command = ProcessServerCommand; + + // REVIEW: When to dispose the timer? + _timer = new Timer(Beat, + null, + _configurationManager.HeartbeatInterval(), + _configurationManager.HeartbeatInterval()); + } + + private TraceSource Trace + { + get + { + return _trace; + } + } + + private void ProcessServerCommand(ServerCommand command) + { + switch (command.ServerCommandType) + { + case ServerCommandType.RemoveConnection: + // Only remove connections if this command didn't originate from the owner + if (!command.IsFromSelf(_serverId)) + { + var connectionId = (string)command.Value; + + // Remove the connection + ConnectionMetadata metadata; + if (_connections.TryGetValue(connectionId, out metadata)) + { + metadata.Connection.End(); + + RemoveConnection(metadata.Connection); + } + } + break; + default: + break; + } + } + + /// + /// Adds a new connection to the list of tracked connections. + /// + /// The connection to be added. + public bool AddConnection(ITrackingConnection connection) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + var newMetadata = new ConnectionMetadata(connection); + bool isNewConnection = true; + + _connections.AddOrUpdate(connection.ConnectionId, newMetadata, (key, old) => + { + Trace.TraceEvent(TraceEventType.Verbose, 0, "Connection {0} exists. Closing previous connection.", old.Connection.ConnectionId); + // Kick out the older connection. This should only happen when + // a previous connection attempt fails on the client side (e.g. transport fallback). + + old.Connection.ApplyState(TransportConnectionStates.Replaced); + + // Don't bother disposing the registration here since the token source + // gets disposed after the request has ended + old.Connection.End(); + + // If we have old metadata this isn't a new connection + isNewConnection = false; + + return newMetadata; + }); + + if (isNewConnection) + { + Trace.TraceInformation("Connection {0} is New.", connection.ConnectionId); + } + + lock (_counterLock) + { + _counters.ConnectionsCurrent.RawValue = _connections.Count; + } + + // Set the initial connection time + newMetadata.Initial = DateTime.UtcNow; + + newMetadata.Connection.ApplyState(TransportConnectionStates.Added); + + return isNewConnection; + } + + /// + /// Removes a connection from the list of tracked connections. + /// + /// The connection to remove. + public void RemoveConnection(ITrackingConnection connection) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + // Remove the connection and associated metadata + ConnectionMetadata metadata; + if (_connections.TryRemove(connection.ConnectionId, out metadata)) + { + lock (_counterLock) + { + _counters.ConnectionsCurrent.RawValue = _connections.Count; + } + + connection.ApplyState(TransportConnectionStates.Removed); + + Trace.TraceInformation("Removing connection {0}", connection.ConnectionId); + } + } + + /// + /// Marks an existing connection as active. + /// + /// The connection to mark. + public void MarkConnection(ITrackingConnection connection) + { + if (connection == null) + { + throw new ArgumentNullException("connection"); + } + + // Do nothing if the connection isn't alive + if (!connection.IsAlive) + { + return; + } + + ConnectionMetadata metadata; + if (_connections.TryGetValue(connection.ConnectionId, out metadata)) + { + metadata.LastMarked = DateTime.UtcNow; + } + } + + public IList GetConnections() + { + return _connections.Values.Select(metadata => metadata.Connection).ToList(); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")] + private void Beat(object state) + { + if (Interlocked.Exchange(ref _running, 1) == 1) + { + Trace.TraceEvent(TraceEventType.Verbose, 0, "Timer handler took longer than current interval"); + return; + } + + lock (_counterLock) + { + _counters.ConnectionsCurrent.RawValue = _connections.Count; + } + + try + { + _heartbeatCount++; + + foreach (var metadata in _connections.Values) + { + if (metadata.Connection.IsAlive) + { + CheckTimeoutAndKeepAlive(metadata); + } + else + { + Trace.TraceEvent(TraceEventType.Verbose, 0, metadata.Connection.ConnectionId + " is dead"); + + // Check if we need to disconnect this connection + CheckDisconnect(metadata); + } + } + } + catch (Exception ex) + { + Trace.TraceEvent(TraceEventType.Error, 0, "SignalR error during transport heart beat on background thread: {0}", ex); + } + finally + { + Interlocked.Exchange(ref _running, 0); + } + } + + private void CheckTimeoutAndKeepAlive(ConnectionMetadata metadata) + { + if (RaiseTimeout(metadata)) + { + // If we're past the expiration time then just timeout the connection + metadata.Connection.Timeout(); + } + else + { + // The connection is still alive so we need to keep it alive with a server side "ping". + // This is for scenarios where networking hardware (proxies, loadbalancers) get in the way + // of us handling timeout's or disconnects gracefully + if (RaiseKeepAlive(metadata)) + { + Trace.TraceEvent(TraceEventType.Verbose, 0, "KeepAlive(" + metadata.Connection.ConnectionId + ")"); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + metadata.Connection.KeepAlive().Catch((ex, state) => OnKeepAliveError(ex, state), Trace); + } + + MarkConnection(metadata.Connection); + } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")] + private void CheckDisconnect(ConnectionMetadata metadata) + { + try + { + if (RaiseDisconnect(metadata)) + { + // Remove the connection from the list + RemoveConnection(metadata.Connection); + + // Fire disconnect on the connection + metadata.Connection.Disconnect(); + } + } + catch (Exception ex) + { + // Swallow exceptions that might happen during disconnect + Trace.TraceEvent(TraceEventType.Error, 0, "Raising Disconnect failed: {0}", ex); + } + } + + private bool RaiseDisconnect(ConnectionMetadata metadata) + { + // The transport is currently dead but it could just be reconnecting + // so we to check it's last active time to see if it's over the disconnect + // threshold + TimeSpan elapsed = DateTime.UtcNow - metadata.LastMarked; + + // The threshold for disconnect is the transport threshold + (potential network issues) + var threshold = metadata.Connection.DisconnectThreshold + _configurationManager.DisconnectTimeout; + + return elapsed >= threshold; + } + + private bool RaiseKeepAlive(ConnectionMetadata metadata) + { + var keepAlive = _configurationManager.KeepAlive; + + // Don't raise keep alive if it's set to 0 or the transport doesn't support + // keep alive + if (keepAlive == null || !metadata.Connection.SupportsKeepAlive) + { + return false; + } + + // Raise keep alive if the keep alive value has passed + return _heartbeatCount % (ulong)ConfigurationExtensions.HeartBeatsPerKeepAlive == 0; + } + + private bool RaiseTimeout(ConnectionMetadata metadata) + { + // The connection already timed out so do nothing + if (metadata.Connection.IsTimedOut) + { + return false; + } + + var keepAlive = _configurationManager.KeepAlive; + // If keep alive is configured and the connection supports keep alive + // don't ever time out + if (keepAlive != null && metadata.Connection.SupportsKeepAlive) + { + return false; + } + + TimeSpan elapsed = DateTime.UtcNow - metadata.Initial; + + // Only raise timeout if we're past the configured connection timeout. + return elapsed >= _configurationManager.ConnectionTimeout; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_timer != null) + { + _timer.Dispose(); + } + + Trace.TraceInformation("Dispose(). Closing all connections"); + + // Kill all connections + foreach (var pair in _connections) + { + ConnectionMetadata metadata; + if (_connections.TryGetValue(pair.Key, out metadata)) + { + metadata.Connection.End(); + } + } + } + } + + public void Dispose() + { + Dispose(true); + } + + private static void OnKeepAliveError(AggregateException ex, object state) + { + ((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to send keep alive: " + ex.GetBaseException()); + } + + private class ConnectionMetadata + { + public ConnectionMetadata(ITrackingConnection connection) + { + Connection = connection; + Initial = DateTime.UtcNow; + LastMarked = DateTime.UtcNow; + } + + // The connection instance + public ITrackingConnection Connection { get; set; } + + // The last time the connection had any activity + public DateTime LastMarked { get; set; } + + // The initial connection time of the connection + public DateTime Initial { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs new file mode 100644 index 000000000..a3619dffc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportManager.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Hosting; + +namespace Microsoft.AspNet.SignalR.Transports +{ + /// + /// The default implementation. + /// + public class TransportManager : ITransportManager + { + private readonly ConcurrentDictionary> _transports = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of class. + /// + /// The default . + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Those are factory methods")] + public TransportManager(IDependencyResolver resolver) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + Register("foreverFrame", context => new ForeverFrameTransport(context, resolver)); + Register("serverSentEvents", context => new ServerSentEventsTransport(context, resolver)); + Register("longPolling", context => new LongPollingTransport(context, resolver)); + Register("webSockets", context => new WebSocketTransport(context, resolver)); + } + + /// + /// Adds a new transport to the list of supported transports. + /// + /// The specified transport. + /// The factory method for the specified transport. + public void Register(string transportName, Func transportFactory) + { + if (String.IsNullOrEmpty(transportName)) + { + throw new ArgumentNullException("transportName"); + } + + if (transportFactory == null) + { + throw new ArgumentNullException("transportFactory"); + } + + _transports.TryAdd(transportName, transportFactory); + } + + /// + /// Removes a transport from the list of supported transports. + /// + /// The specified transport. + public void Remove(string transportName) + { + if (String.IsNullOrEmpty(transportName)) + { + throw new ArgumentNullException("transportName"); + } + + Func removed; + _transports.TryRemove(transportName, out removed); + } + + /// + /// Gets the specified transport for the specified . + /// + /// The for the current request. + /// The for the specified . + public ITransport GetTransport(HostContext hostContext) + { + if (hostContext == null) + { + throw new ArgumentNullException("hostContext"); + } + + string transportName = hostContext.Request.QueryString["transport"]; + + if (String.IsNullOrEmpty(transportName)) + { + return null; + } + + Func factory; + if (_transports.TryGetValue(transportName, out factory)) + { + return factory(hostContext); + } + + return null; + } + + /// + /// Determines whether the specified transport is supported. + /// + /// The name of the transport to test. + /// True if the transport is supported, otherwise False. + public bool SupportsTransport(string transportName) + { + if (String.IsNullOrEmpty(transportName)) + { + return false; + } + + return _transports.ContainsKey(transportName); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs new file mode 100644 index 000000000..66b4838e3 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/WebSocketTransport.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Json; +using Microsoft.AspNet.SignalR.Tracing; + +namespace Microsoft.AspNet.SignalR.Transports +{ + public class WebSocketTransport : ForeverTransport + { + private readonly HostContext _context; + private IWebSocket _socket; + private bool _isAlive = true; + + private readonly Action _message; + private readonly Action _closed; + private readonly Action _error; + + public WebSocketTransport(HostContext context, + IDependencyResolver resolver) + : this(context, + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve(), + resolver.Resolve()) + { + } + + public WebSocketTransport(HostContext context, + IJsonSerializer serializer, + ITransportHeartbeat heartbeat, + IPerformanceCounterManager performanceCounterWriter, + ITraceManager traceManager) + : base(context, serializer, heartbeat, performanceCounterWriter, traceManager) + { + _context = context; + _message = OnMessage; + _closed = OnClosed; + _error = OnError; + } + + public override bool IsAlive + { + get + { + return _isAlive; + } + } + + public override CancellationToken CancellationToken + { + get + { + return CancellationToken.None; + } + } + + public override Task KeepAlive() + { + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(state => + { + var webSocket = (IWebSocket)state; + return webSocket.Send("{}"); + }, + _socket); + } + + public override Task ProcessRequest(ITransportConnection connection) + { + var webSocketRequest = _context.Request as IWebSocketRequest; + + // Throw if the server implementation doesn't support websockets + if (webSocketRequest == null) + { + throw new InvalidOperationException(Resources.Error_WebSocketsNotSupported); + } + + return webSocketRequest.AcceptWebSocketRequest(socket => + { + _socket = socket; + socket.OnClose = _closed; + socket.OnMessage = _message; + socket.OnError = _error; + + return ProcessRequestCore(connection); + }); + } + + protected override TextWriter CreateResponseWriter() + { + return new BufferTextWriter(_socket); + } + + public override Task Send(object value) + { + var context = new WebSocketTransportContext(this, value); + + // Ensure delegate continues to use the C# Compiler static delegate caching optimization. + return EnqueueOperation(state => PerformSend(state), context); + } + + public override Task Send(PersistentResponse response) + { + OnSendingResponse(response); + + return Send((object)response); + } + + private static Task PerformSend(object state) + { + var context = (WebSocketTransportContext)state; + + context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter); + context.Transport.OutputWriter.Flush(); + + return context.Transport._socket.Flush(); + } + + private void OnMessage(string message) + { + if (Received != null) + { + Received(message).Catch(); + } + } + + private void OnClosed(bool clean) + { + Trace.TraceInformation("CloseSocket({0}, {1})", clean, ConnectionId); + + // If we performed a clean disconnect then we go through the normal disconnect routine. However, + // If we performed an unclean disconnect we want to mark the connection as "not alive" and let the + // HeartBeat clean it up. This is to maintain consistency across the transports. + if (clean) + { + Abort(); + } + + _isAlive = false; + } + + private void OnError(Exception error) + { + Trace.TraceError("OnError({0}, {1})", ConnectionId, error); + } + + private class WebSocketTransportContext + { + public WebSocketTransport Transport; + public object State; + + public WebSocketTransportContext(WebSocketTransport transport, object state) + { + Transport = transport; + State = state; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Core/packages.config b/src/Microsoft.AspNet.SignalR.Core/packages.config new file mode 100644 index 000000000..64c6dfc71 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Core/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs new file mode 100644 index 000000000..a2234051b --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Handlers/CallHandler.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin +{ + public class CallHandler + { + private readonly ConnectionConfiguration _configuration; + private readonly PersistentConnection _connection; + + public CallHandler(ConnectionConfiguration configuration, PersistentConnection connection) + { + _configuration = configuration; + _connection = connection; + } + + public Task Invoke(IDictionary environment) + { + var serverRequest = new ServerRequest(environment); + var serverResponse = new ServerResponse(environment); + var hostContext = new HostContext(serverRequest, serverResponse); + + string origin = serverRequest.RequestHeaders.GetHeader("Origin"); + + if (_configuration.EnableCrossDomain) + { + // Add CORS response headers support + if (!String.IsNullOrEmpty(origin)) + { + serverResponse.ResponseHeaders.SetHeader("Access-Control-Allow-Origin", origin); + serverResponse.ResponseHeaders.SetHeader("Access-Control-Allow-Credentials", "true"); + } + } + else + { + string callback = serverRequest.QueryString["callback"]; + + // If it's a JSONP request and we're not allowing cross domain requests then block it + // If there's an origin header and it's not a same origin request then block it. + + if (!String.IsNullOrEmpty(callback) || + (!String.IsNullOrEmpty(origin) && !IsSameOrigin(serverRequest.Url, origin))) + { + return EndResponse(environment, 403, Resources.Forbidden_CrossDomainIsDisabled); + } + } + + // Add the nosniff header for all responses to prevent IE from trying to sniff mime type from contents + serverResponse.ResponseHeaders.SetHeader("X-Content-Type-Options", "nosniff"); + + // REVIEW: Performance + hostContext.Items[HostConstants.SupportsWebSockets] = environment.SupportsWebSockets(); + hostContext.Items[HostConstants.ShutdownToken] = environment.GetShutdownToken(); + hostContext.Items[HostConstants.DebugMode] = environment.GetIsDebugEnabled(); + + serverRequest.DisableRequestCompression(); + serverResponse.DisableResponseBuffering(); + + _connection.Initialize(_configuration.Resolver, hostContext); + + if (!_connection.Authorize(serverRequest)) + { + // If we failed to authorize the request then return a 403 since the request + // can't do anything + return EndResponse(environment, 403, "Forbidden"); + } + else + { + return _connection.ProcessRequest(hostContext); + } + } + + private static Task EndResponse(IDictionary environment, int statusCode, string reason) + { + environment[OwinConstants.ResponseStatusCode] = statusCode; + environment[OwinConstants.ResponseReasonPhrase] = reason; + + return TaskAsyncHelper.Empty; + } + + private static bool IsSameOrigin(Uri requestUri, string origin) + { + Uri originUri; + if (!Uri.TryCreate(origin.Trim(), UriKind.Absolute, out originUri)) + { + return false; + } + + return (requestUri.Scheme == originUri.Scheme) && + (requestUri.Host == originUri.Host) && + (requestUri.Port == originUri.Port); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs new file mode 100644 index 000000000..fcf83e5e3 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Handlers/HubDispatcherHandler.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hubs; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin.Handlers +{ + using AppFunc = Func, Task>; + + public class HubDispatcherHandler + { + private readonly AppFunc _next; + private readonly string _path; + private readonly HubConfiguration _configuration; + + public HubDispatcherHandler(AppFunc next, string path, HubConfiguration configuration) + { + _next = next; + _path = path; + _configuration = configuration; + } + + public Task Invoke(IDictionary environment) + { + var path = environment.Get(OwinConstants.RequestPath); + if (path == null || !PrefixMatcher.IsMatch(_path, path)) + { + return _next(environment); + } + + var dispatcher = new HubDispatcher(_configuration); + + var handler = new CallHandler(_configuration, dispatcher); + return handler.Invoke(environment); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs b/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs new file mode 100644 index 000000000..71fb299eb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Handlers/PersistentConnectionHandler.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin.Handlers +{ + using AppFunc = Func, Task>; + + public class PersistentConnectionHandler + { + private readonly AppFunc _next; + private readonly string _path; + private readonly Type _connectionType; + private readonly ConnectionConfiguration _configuration; + + public PersistentConnectionHandler(AppFunc next, string path, Type connectionType, ConnectionConfiguration configuration) + { + _next = next; + _path = path; + _connectionType = connectionType; + _configuration = configuration; + } + + public Task Invoke(IDictionary environment) + { + var path = environment.Get(OwinConstants.RequestPath); + if (path == null || !PrefixMatcher.IsMatch(_path, path)) + { + return _next(environment); + } + + var connectionFactory = new PersistentConnectionFactory(_configuration.Resolver); + var connection = connectionFactory.CreateInstance(_connectionType); + + var handler = new CallHandler(_configuration, connection); + return handler.Invoke(environment); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs new file mode 100644 index 000000000..ed5e9563f --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/Headers.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.SignalR.Owin.Infrastructure +{ + /// + /// Helper methods for creating and consuming CallParameters.Headers and ResultParameters.Headers. + /// + internal static class Headers + { + public static IDictionary SetHeader(this IDictionary headers, + string name, string value) + { + headers[name] = new[] { value }; + return headers; + } + + public static string[] GetHeaders(this IDictionary headers, + string name) + { + string[] value; + return headers != null && headers.TryGetValue(name, out value) ? value : null; + } + + public static string GetHeader(this IDictionary headers, + string name) + { + var values = GetHeaders(headers, name); + if (values == null) + { + return null; + } + + switch (values.Length) + { + case 0: + return String.Empty; + case 1: + return values[0]; + default: + return String.Join(",", values); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs new file mode 100644 index 000000000..e2c246ba1 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinConstants.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +namespace Microsoft.AspNet.SignalR.Owin +{ + internal static class OwinConstants + { + public const string Version = "owin.Version"; + + public const string RequestBody = "owin.RequestBody"; + public const string RequestHeaders = "owin.RequestHeaders"; + public const string RequestScheme = "owin.RequestScheme"; + public const string RequestMethod = "owin.RequestMethod"; + public const string RequestPathBase = "owin.RequestPathBase"; + public const string RequestPath = "owin.RequestPath"; + public const string RequestQueryString = "owin.RequestQueryString"; + public const string RequestProtocol = "owin.RequestProtocol"; + + public const string CallCancelled = "owin.CallCancelled"; + + public const string ResponseStatusCode = "owin.ResponseStatusCode"; + public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; + public const string ResponseHeaders = "owin.ResponseHeaders"; + public const string ResponseBody = "owin.ResponseBody"; + + public const string TraceOutput = "host.TraceOutput"; + + public const string User = "server.User"; + public const string RemoteIpAddress = "server.RemoteIpAddress"; + public const string RemotePort = "server.RemotePort"; + public const string LocalIpAddress = "server.LocalIpAddress"; + public const string LocalPort = "server.LocalPort"; + + public const string DisableRequestCompression = "systemweb.DisableResponseCompression"; + public const string DisableRequestBuffering = "server.DisableRequestBuffering"; + public const string DisableResponseBuffering = "server.DisableResponseBuffering"; + + public const string ServerCapabilities = "server.Capabilities"; + public const string WebSocketVersion = "websocket.Version"; + public const string WebSocketAccept = "websocket.Accept"; + + public const string HostOnAppDisposing = "host.OnAppDisposing"; + public const string HostAppNameKey = "host.AppName"; + public const string HostAppModeKey = "host.AppMode"; + public const string AppModeDevelopment = "development"; + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs new file mode 100644 index 000000000..26459c76c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/OwinEnvironmentExtensions.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Owin +{ + internal static class OwinEnvironmentExtensions + { + internal static T Get(this IDictionary environment, string key) + { + object value; + return environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + internal static CancellationToken GetShutdownToken(this IDictionary env) + { + object value; + return env.TryGetValue(OwinConstants.HostOnAppDisposing, out value) + && value is CancellationToken + ? (CancellationToken)value + : default(CancellationToken); + } + + internal static string GetAppInstanceName(this IDictionary environment) + { + object value; + if (environment.TryGetValue(OwinConstants.HostAppNameKey, out value)) + { + var stringVal = value as string; + + if (!String.IsNullOrEmpty(stringVal)) + { + return stringVal; + } + } + + return null; + } + + internal static bool SupportsWebSockets(this IDictionary environment) + { + object value; + if (environment.TryGetValue(OwinConstants.ServerCapabilities, out value)) + { + var capabilities = value as IDictionary; + if (capabilities != null) + { + return capabilities.ContainsKey(OwinConstants.WebSocketVersion); + } + } + return false; + } + + internal static bool GetIsDebugEnabled(this IDictionary environment) + { + object value; + if (environment.TryGetValue(OwinConstants.HostAppModeKey, out value)) + { + var stringVal = value as string; + return !String.IsNullOrWhiteSpace(stringVal) && + OwinConstants.AppModeDevelopment.Equals(stringVal, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs new file mode 100644 index 000000000..18b159d4d --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/ParamDictionary.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.SignalR.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin.Infrastructure +{ + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "It is instantiated in the static Parse method")] + internal sealed class ParamDictionary + { + private static readonly char[] DefaultParamSeparators = new[] { '&', ';' }; + private static readonly char[] ParamKeyValueSeparator = new[] { '=' }; + private static readonly char[] LeadingWhitespaceChars = new[] { ' ' }; + + internal static IEnumerable> ParseToEnumerable(string value, char[] delimiters = null) + { + value = value ?? String.Empty; + delimiters = delimiters ?? DefaultParamSeparators; + + var items = value.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); + + foreach (var item in items) + { + string[] pair = item.Split(ParamKeyValueSeparator, 2, StringSplitOptions.None); + + string pairKey = UrlDecoder.UrlDecode(pair[0]).TrimStart(LeadingWhitespaceChars); + string pairValue = pair.Length < 2 ? String.Empty : UrlDecoder.UrlDecode(pair[1]); + + yield return new KeyValuePair(pairKey, pairValue); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs new file mode 100644 index 000000000..73186522a --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/PrefixMatcher.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace Microsoft.AspNet.SignalR.Owin.Infrastructure +{ + internal static class PrefixMatcher + { + public static bool IsMatch(string pathBase, string path) + { + pathBase = EnsureStartsWithSlash(pathBase); + path = EnsureStartsWithSlash(path); + + var pathLength = path.Length; + var pathBaseLength = pathBase.Length; + + if (pathLength < pathBaseLength) + { + return false; + } + + if (pathLength > pathBaseLength && path[pathBaseLength] != '/') + { + return false; + } + + if (!path.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + private static string EnsureStartsWithSlash(string path) + { + if (path.Length == 0) + { + return path; + } + + if (path[0] == '/') + { + return path; + } + + return '/' + path; + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs new file mode 100644 index 000000000..0d813a875 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Infrastructure/UrlDecoder.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Text; + +namespace Microsoft.AspNet.SignalR.Infrastructure +{ + // Taken from System.Net.Http.Formatting.Internal.UrlDecoder.cs (http://aspnetwebstack.codeplex.com/) + + /// + /// Helpers for decoding URI query components. + /// + internal static class UrlDecoder + { + // The implementation below is ported from WebUtility for use in .Net 4 + + public static string UrlDecode(string str) + { + if (str == null) + return null; + + return UrlDecodeInternal(str, Encoding.UTF8); + } + + #region UrlDecode implementation + + private static string UrlDecodeInternal(string value, Encoding encoding) + { + if (value == null) + { + return null; + } + + int count = value.Length; + var helper = new DecoderHelper(count, encoding); + + // go through the string's chars collapsing %XX and %uXXXX and + // appending each char as char, with exception of %XX constructs + // that are appended as bytes + + for (int pos = 0; pos < count; pos++) + { + char ch = value[pos]; + + if (ch == '+') + { + ch = ' '; + } + else if (ch == '%' && pos < count - 2) + { + int h1 = HexToInt(value[pos + 1]); + int h2 = HexToInt(value[pos + 2]); + + if (h1 >= 0 && h2 >= 0) + { // valid 2 hex chars + byte b = (byte)((h1 << 4) | h2); + pos += 2; + + // don't add as char + helper.AddByte(b); + continue; + } + } + + if ((ch & 0xFF80) == 0) + helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode + else + helper.AddChar(ch); + } + + return helper.GetString(); + } + + private static int HexToInt(char h) + { + return (h >= '0' && h <= '9') ? h - '0' : + (h >= 'a' && h <= 'f') ? h - 'a' + 10 : + (h >= 'A' && h <= 'F') ? h - 'A' + 10 : + -1; + } + + #endregion + + #region DecoderHelper nested class + + // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes + private class DecoderHelper + { + private int _bufferSize; + + // Accumulate characters in a special array + private int _numChars; + private char[] _charBuffer; + + // Accumulate bytes for decoding into characters in a special array + private int _numBytes; + private byte[] _byteBuffer; + + // Encoding to convert chars to bytes + private Encoding _encoding; + + private void FlushBytes() + { + if (_numBytes > 0) + { + _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); + _numBytes = 0; + } + } + + internal DecoderHelper(int bufferSize, Encoding encoding) + { + _bufferSize = bufferSize; + _encoding = encoding; + + _charBuffer = new char[bufferSize]; + // byte buffer created on demand + } + + internal void AddChar(char ch) + { + if (_numBytes > 0) + FlushBytes(); + + _charBuffer[_numChars++] = ch; + } + + internal void AddByte(byte b) + { + if (_byteBuffer == null) + _byteBuffer = new byte[_bufferSize]; + + _byteBuffer[_numBytes++] = b; + } + + internal String GetString() + { + if (_numBytes > 0) + FlushBytes(); + + if (_numChars > 0) + return new String(_charBuffer, 0, _numChars); + else + return String.Empty; + } + } + + #endregion + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj new file mode 100644 index 000000000..ebc89fefc --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522} + Library + Properties + Microsoft.AspNet.SignalR.Owin + Microsoft.AspNet.SignalR.Owin + v4.0 + 512 + ..\..\ + true + + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + bin\Debug\Microsoft.AspNet.SignalR.Owin.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + bin\Release\Microsoft.AspNet.SignalR.Owin.XML + + + + False + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\CommonVersionInfo.cs + + + Infrastructure\TaskAsyncHelper.cs + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} + Microsoft.AspNet.SignalR.Core + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings new file mode 100644 index 000000000..5b8822215 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Microsoft.AspNet.SignalR.Owin.csproj.DotSettings @@ -0,0 +1,2 @@ + + DO_NOT_SHOW \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs new file mode 100644 index 000000000..7ff9c5d8c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/OwinExtensions.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Owin; +using Microsoft.AspNet.SignalR.Owin.Handlers; + +namespace Owin +{ + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Owin", Justification = "The owin namespace is for consistentcy.")] + public static class OwinExtensions + { + public static IAppBuilder MapHubs(this IAppBuilder builder) + { + return builder.MapHubs(new HubConfiguration()); + } + + public static IAppBuilder MapHubs(this IAppBuilder builder, HubConfiguration configuration) + { + return builder.MapHubs("/signalr", configuration); + } + + public static IAppBuilder MapHubs(this IAppBuilder builder, string path, HubConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + return builder.UseType(path, configuration); + } + + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The type parameter is syntactic sugar")] + public static IAppBuilder MapConnection(this IAppBuilder builder, string url) where T : PersistentConnection + { + return builder.MapConnection(url, typeof(T), new ConnectionConfiguration()); + } + + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The type parameter is syntactic sugar")] + public static IAppBuilder MapConnection(this IAppBuilder builder, string url, ConnectionConfiguration configuration) where T : PersistentConnection + { + return builder.MapConnection(url, typeof(T), configuration); + } + + public static IAppBuilder MapConnection(this IAppBuilder builder, string url, Type connectionType, ConnectionConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + return builder.UseType(url, connectionType, configuration); + } + + private static IAppBuilder UseType(this IAppBuilder builder, params object[] args) + { + if (args.Length > 0) + { + var configuration = args[args.Length - 1] as ConnectionConfiguration; + + if (configuration == null) + { + throw new ArgumentException(Resources.Error_NoConfiguration); + } + + var resolver = configuration.Resolver; + + if (resolver == null) + { + throw new ArgumentException(Resources.Error_NoDepenendeyResolver); + } + + var env = builder.Properties; + CancellationToken token = env.GetShutdownToken(); + string instanceName = env.GetAppInstanceName(); + + resolver.InitializeHost(instanceName, token); + } + + return builder.Use(typeof(T), args); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..c0e6f23c4 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyTitle("Microsoft.AspNet.SignalR.Owin")] +[assembly: AssemblyDescription("Assembly containing default SignalR host.")] diff --git a/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs b/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs new file mode 100644 index 000000000..03676f853 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/RequestExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.SignalR.Owin; + +namespace Microsoft.AspNet.SignalR +{ + public static class RequestExtensions + { + public static T GetOwinVariable(this IRequest request, string key) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + var env = request.Items.Get>(ServerRequest.OwinEnvironmentKey); + + return env == null ? default(T) : env.Get(key); + } + + private static T Get(this IDictionary values, string key) + { + object value; + return values.TryGetValue(key, out value) ? (T)value : default(T); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs b/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs new file mode 100644 index 000000000..8160cb100 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Resources.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18010 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.SignalR.Owin { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.SignalR.Owin.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A configuration object must be specified.. + /// + internal static string Error_NoConfiguration { + get { + return ResourceManager.GetString("Error_NoConfiguration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A dependency resolver must be specified.. + /// + internal static string Error_NoDepenendeyResolver { + get { + return ResourceManager.GetString("Error_NoDepenendeyResolver", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not a valid web socket request.. + /// + internal static string Error_NotWebSocketRequest { + get { + return ResourceManager.GetString("Error_NotWebSocketRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Forbidden: SignalR cross domain is disabled.. + /// + internal static string Forbidden_CrossDomainIsDisabled { + get { + return ResourceManager.GetString("Forbidden_CrossDomainIsDisabled", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/Resources.resx b/src/Microsoft.AspNet.SignalR.Owin/Resources.resx new file mode 100644 index 000000000..c77be7797 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A configuration object must be specified. + + + A dependency resolver must be specified. + + + Not a valid web socket request. + + + Forbidden: SignalR cross domain is disabled. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs new file mode 100644 index 000000000..407b7eb98 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.Owin.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin +{ + public partial class ServerRequest + { + private readonly IDictionary _environment; + + public static readonly string OwinEnvironmentKey = "owin.environment"; + + public ServerRequest(IDictionary environment) + { + _environment = environment; + + Items = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { OwinEnvironmentKey , _environment } + }; + } + + private string RequestMethod + { + get { return _environment.Get(OwinConstants.RequestMethod); } + } + + public IDictionary RequestHeaders + { + get { return _environment.Get>(OwinConstants.RequestHeaders); } + } + + private Stream RequestBody + { + get { return _environment.Get(OwinConstants.RequestBody); } + } + + private string RequestScheme + { + get { return _environment.Get(OwinConstants.RequestScheme); } + } + + private string RequestPathBase + { + get { return _environment.Get(OwinConstants.RequestPathBase); } + } + + private string RequestPath + { + get { return _environment.Get(OwinConstants.RequestPath); } + } + + private string RequestQueryString + { + get { return _environment.Get(OwinConstants.RequestQueryString); } + } + + public Action DisableRequestCompression + { + get { return _environment.Get(OwinConstants.DisableRequestCompression) ?? (() => { }); } + } + + private bool TryParseHostHeader(out IPAddress address, out string host, out int port) + { + address = null; + host = null; + port = -1; + + var hostHeader = RequestHeaders.GetHeader("Host"); + if (String.IsNullOrWhiteSpace(hostHeader)) + { + return false; + } + + // IPv6 (http://www.ietf.org/rfc/rfc2732.txt) + if (hostHeader.StartsWith("[", StringComparison.Ordinal)) + { + var portIndex = hostHeader.LastIndexOf("]:", StringComparison.Ordinal); + if (portIndex != -1 && Int32.TryParse(hostHeader.Substring(portIndex + 2), out port)) + { + if (IPAddress.TryParse(hostHeader.Substring(1, portIndex - 1), out address)) + { + host = null; + return true; + } + host = hostHeader.Substring(0, portIndex + 1); + return true; + } + if (hostHeader.EndsWith("]", StringComparison.Ordinal)) + { + if (IPAddress.TryParse(hostHeader.Substring(1, hostHeader.Length - 2), out address)) + { + host = null; + port = -1; + return true; + } + } + } + else + { + // IPAddresses + if (IPAddress.TryParse(hostHeader, out address)) + { + host = null; + port = -1; + return true; + } + + var portIndex = hostHeader.LastIndexOf(':'); + if (portIndex != -1 && Int32.TryParse(hostHeader.Substring(portIndex + 1), out port)) + { + host = hostHeader.Substring(0, portIndex); + return true; + } + } + + // Plain + host = hostHeader; + return true; + } + + private string RequestHost + { + get + { + IPAddress address; + string host; + int port; + if (TryParseHostHeader(out address, out host, out port)) + { + return host ?? address.ToString(); + } + return _environment.Get(OwinConstants.LocalIpAddress) ?? IPAddress.Loopback.ToString(); + } + } + + private int RequestPort + { + get + { + IPAddress address; + string host; + int port; + if (TryParseHostHeader(out address, out host, out port)) + { + if (port == -1) + { + return DefaultPort; + } + return port; + } + + var portString = _environment.Get(OwinConstants.LocalPort); + if (Int32.TryParse(portString, out port) && port != 0) + { + return port; + } + + return DefaultPort; + } + } + + private int DefaultPort + { + get + { + return String.Equals(RequestScheme, "https", StringComparison.OrdinalIgnoreCase) ? 443 : 80; + } + } + + private string ContentType + { + get + { + return RequestHeaders.GetHeader("Content-Type"); + } + } + + private string MediaType + { + get + { + var contentType = ContentType; + if (contentType == null) + { + return null; + } + + var delimiterPos = contentType.IndexOfAny(CommaSemicolon); + return delimiterPos < 0 ? contentType : contentType.Substring(0, delimiterPos); + } + } + + private bool HasFormData + { + get + { + var mediaType = MediaType; + return (RequestMethod == "POST" && String.IsNullOrEmpty(mediaType)) + || mediaType == "application/x-www-form-urlencoded" + || mediaType == "multipart/form-data"; + } + } + + private bool HasParseableData + { + get + { + var mediaType = MediaType; + return mediaType == "application/x-www-form-urlencoded" + || mediaType == "multipart/form-data"; + } + } + + private IEnumerable> ReadForm() + { + if (!HasFormData && !HasParseableData) + { + return Enumerable.Empty>(); + } + + var body = RequestBody; + if (body.CanSeek) + { + body.Seek(0, SeekOrigin.Begin); + } + + var text = new StreamReader(body).ReadToEnd(); + return ParamDictionary.ParseToEnumerable(text); + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs new file mode 100644 index 000000000..399c8dc5c --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/ServerRequest.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; +using Microsoft.AspNet.SignalR.Hosting; + +namespace Microsoft.AspNet.SignalR.Owin +{ + using WebSocketFunc = Func, Task>; + public partial class ServerRequest : +#if NET45 + IWebSocketRequest +#else + IRequest +#endif + { + private static readonly char[] CommaSemicolon = new[] { ',', ';' }; + + private Uri _url; + private NameValueCollection _queryString; + private NameValueCollection _headers; + private NameValueCollection _form; + private bool _formInitialized; + private object _formLock = new object(); + private IDictionary _cookies; + + public Uri Url + { + get + { + return LazyInitializer.EnsureInitialized( + ref _url, () => + { + var uriBuilder = new UriBuilder(RequestScheme, RequestHost, RequestPort, RequestPathBase + RequestPath); + if (!String.IsNullOrEmpty(RequestQueryString)) + { + uriBuilder.Query = RequestQueryString; + } + return uriBuilder.Uri; + }); + } + } + + + public NameValueCollection QueryString + { + get + { + return LazyInitializer.EnsureInitialized( + ref _queryString, () => + { + var collection = new NameValueCollection(); + foreach (var kv in ParamDictionary.ParseToEnumerable(RequestQueryString)) + { + collection.Add(kv.Key, kv.Value); + } + return collection; + }); + } + } + + public NameValueCollection Headers + { + get + { + return LazyInitializer.EnsureInitialized( + ref _headers, () => + { + var collection = new NameValueCollection(); + foreach (var kv in RequestHeaders) + { + if (kv.Value != null) + { + for (var index = 0; index != kv.Value.Length; ++index) + { + collection.Add(kv.Key, kv.Value[index]); + } + } + } + return collection; + }); + } + } + + public NameValueCollection Form + { + get + { + return LazyInitializer.EnsureInitialized( + ref _form, ref _formInitialized, ref _formLock, () => + { + var collection = new NameValueCollection(); + foreach (var kv in ReadForm()) + { + collection.Add(kv.Key, kv.Value); + } + return collection; + }); + } + } + + + public IDictionary Cookies + { + get + { + return LazyInitializer.EnsureInitialized( + ref _cookies, () => + { + var cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); + var text = RequestHeaders.GetHeader("Cookie"); + foreach (var kv in ParamDictionary.ParseToEnumerable(text, CommaSemicolon)) + { + if (!cookies.ContainsKey(kv.Key)) + { + cookies.Add(kv.Key, new Cookie(kv.Key, kv.Value)); + } + } + return cookies; + }); + } + } + + public IPrincipal User + { + get { return _environment.Get(OwinConstants.User); } + } + + + public IDictionary Items + { + get; + private set; + } + +#if NET45 + public Task AcceptWebSocketRequest(Func callback) + { + var accept = _environment.Get, WebSocketFunc>>(OwinConstants.WebSocketAccept); + if (accept == null) + { + throw new InvalidOperationException(Resources.Error_NotWebSocketRequest); + } + + var handler = new OwinWebSocketHandler(callback); + accept(null, handler.ProcessRequestAsync); + return TaskAsyncHelper.Empty; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs b/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs new file mode 100644 index 000000000..bdf7a5ba6 --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/ServerResponse.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR.Hosting; +using Microsoft.AspNet.SignalR.Owin.Infrastructure; + +namespace Microsoft.AspNet.SignalR.Owin +{ + public partial class ServerResponse : IResponse + { + private readonly CancellationToken _callCancelled; + private readonly IDictionary _environment; + private Stream _responseBody; + + public ServerResponse(IDictionary environment) + { + _environment = environment; + _callCancelled = _environment.Get(OwinConstants.CallCancelled); + } + + public CancellationToken CancellationToken + { + get { return _callCancelled; } + } + + public string ContentType + { + get { return ResponseHeaders.GetHeader("Content-Type"); } + set { ResponseHeaders.SetHeader("Content-Type", value); } + } + + public void Write(ArraySegment data) + { + ResponseBody.Write(data.Array, data.Offset, data.Count); + } + + public Task Flush() + { +#if NET45 + return ResponseBody.FlushAsync(); +#else + return TaskAsyncHelper.FromMethod(() => ResponseBody.Flush()); +#endif + } + + public Task End() + { + return TaskAsyncHelper.Empty; + } + + public IDictionary ResponseHeaders + { + get { return _environment.Get>(OwinConstants.ResponseHeaders); } + } + + public Stream ResponseBody + { + get + { + if (_responseBody == null) + { + _responseBody = _environment.Get(OwinConstants.ResponseBody); + } + + return _responseBody; + } + } + + public Action DisableResponseBuffering + { + get { return _environment.Get(OwinConstants.DisableResponseBuffering) ?? (() => { }); } + } + } +} diff --git a/src/Microsoft.AspNet.SignalR.Owin/packages.config b/src/Microsoft.AspNet.SignalR.Owin/packages.config new file mode 100644 index 000000000..ac23ae5cb --- /dev/null +++ b/src/Microsoft.AspNet.SignalR.Owin/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NzbDrone.Api/packages.config b/src/NzbDrone.Api/packages.config index 66d291dc8..bc189533c 100644 --- a/src/NzbDrone.Api/packages.config +++ b/src/NzbDrone.Api/packages.config @@ -1,7 +1,6 @@  - diff --git a/src/NzbDrone.Console/NzbDrone.Console.csproj b/src/NzbDrone.Console/NzbDrone.Console.csproj index dedc46e2f..5c58201d9 100644 --- a/src/NzbDrone.Console/NzbDrone.Console.csproj +++ b/src/NzbDrone.Console/NzbDrone.Console.csproj @@ -63,14 +63,6 @@ OnBuildSuccess - - False - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll - - - False - ..\packages\Microsoft.AspNet.SignalR.Owin.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Owin.dll - False ..\packages\Microsoft.Owin.1.1.0-beta2\lib\net40\Microsoft.Owin.dll @@ -125,6 +117,14 @@ + + {1b9a82c4-bca1-4834-a33e-226f17be070b} + Microsoft.AspNet.SignalR.Core + + + {2b8c6dad-4d85-41b1-83fd-248d9f347522} + Microsoft.AspNet.SignalR.Owin + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} NzbDrone.Common diff --git a/src/NzbDrone.Console/packages.config b/src/NzbDrone.Console/packages.config index 8c5b8f6f8..abc6822a4 100644 --- a/src/NzbDrone.Console/packages.config +++ b/src/NzbDrone.Console/packages.config @@ -1,7 +1,5 @@  - - diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index b9d21274f..4cc61b5ce 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -69,14 +69,6 @@ ..\Libraries\Interop.NetFwTypeLib.dll True - - False - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll - - - False - ..\packages\Microsoft.AspNet.SignalR.Owin.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Owin.dll - False ..\packages\Microsoft.Owin.1.1.0-beta2\lib\net40\Microsoft.Owin.dll @@ -176,6 +168,14 @@ + + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} + Microsoft.AspNet.SignalR.Core + + + {2b8c6dad-4d85-41b1-83fd-248d9f347522} + Microsoft.AspNet.SignalR.Owin + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2} NzbDrone.Api diff --git a/src/NzbDrone.Host/packages.config b/src/NzbDrone.Host/packages.config index 941d8db76..d9d7f38c3 100644 --- a/src/NzbDrone.Host/packages.config +++ b/src/NzbDrone.Host/packages.config @@ -1,7 +1,5 @@  - - diff --git a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj index efbda7398..f1c7c9b02 100644 --- a/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj +++ b/src/NzbDrone.SignalR/NzbDrone.SignalR.csproj @@ -33,9 +33,6 @@ MinimumRecommendedRules.ruleset - - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll - False ..\packages\Newtonsoft.Json.5.0.8\lib\net40\Newtonsoft.Json.dll @@ -60,6 +57,10 @@ + + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} + Microsoft.AspNet.SignalR.Core + {f2be0fdf-6e47-4827-a420-dd4ef82407f8} NzbDrone.Common diff --git a/src/NzbDrone.SignalR/packages.config b/src/NzbDrone.SignalR/packages.config index a5b443e7a..b47d3a3ee 100644 --- a/src/NzbDrone.SignalR/packages.config +++ b/src/NzbDrone.SignalR/packages.config @@ -1,5 +1,4 @@  - \ No newline at end of file diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index d7333e186..09a409519 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -62,6 +62,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.SignalR", "NzbDron EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Automation.Test", "NzbDrone.Automation.Test\NzbDrone.Automation.Test.csproj", "{CC26800D-F67E-464B-88DE-8EB1A0C227A3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Core", "Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj", "{1B9A82C4-BCA1-4834-A33E-226F17BE070B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Owin", "Microsoft.AspNet.SignalR.Owin\Microsoft.AspNet.SignalR.Owin.csproj", "{2B8C6DAD-4D85-41B1-83FD-248D9F347522}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -292,6 +298,26 @@ Global {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Mixed Platforms.Build.0 = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Any CPU.Build.0 = Release|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.ActiveCfg = Release|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Any CPU.Build.0 = Release|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -313,6 +339,9 @@ Global {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976} = {486ADF86-DD89-4E19-B805-9D94F19800D9} {95C11A9E-56ED-456A-8447-2C89C1139266} = {486ADF86-DD89-4E19-B805-9D94F19800D9} {D12F7F2F-8A3C-415F-88FA-6DD061A84869} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {2B8C6DAD-4D85-41B1-83FD-248D9F347522} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 diff --git a/src/NzbDrone.sln.DotSettings b/src/NzbDrone.sln.DotSettings index a7de7ce4f..e67821bf5 100644 --- a/src/NzbDrone.sln.DotSettings +++ b/src/NzbDrone.sln.DotSettings @@ -38,10 +38,26 @@ COLOR True SOLUTION_FOLDER + + + True C:\Dropbox\Git\NzbDrone\NzbDrone.sln.DotSettings + True + C:\Dropbox\Git\NzbDrone\src\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj.DotSettings + ..\Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj.DotSettings + + + + + True 1 + True + 3 + + + True 2 False diff --git a/src/NzbDrone/NzbDrone.csproj b/src/NzbDrone/NzbDrone.csproj index 7111bc03a..5bbd19bef 100644 --- a/src/NzbDrone/NzbDrone.csproj +++ b/src/NzbDrone/NzbDrone.csproj @@ -63,14 +63,6 @@ OnOutputUpdated - - False - ..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll - - - False - ..\packages\Microsoft.AspNet.SignalR.Owin.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Owin.dll - False ..\packages\Microsoft.Owin.1.1.0-beta2\lib\net40\Microsoft.Owin.dll @@ -135,6 +127,14 @@ + + {1b9a82c4-bca1-4834-a33e-226f17be070b} + Microsoft.AspNet.SignalR.Core + + + {2b8c6dad-4d85-41b1-83fd-248d9f347522} + Microsoft.AspNet.SignalR.Owin + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8} NzbDrone.Common diff --git a/src/NzbDrone/packages.config b/src/NzbDrone/packages.config index 8c5b8f6f8..abc6822a4 100644 --- a/src/NzbDrone/packages.config +++ b/src/NzbDrone/packages.config @@ -1,7 +1,5 @@  - -