diff --git a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.core.js b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.core.js index 6abf4e3dbf..673dbce32e 100644 --- a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.core.js +++ b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.core.js @@ -100,7 +100,7 @@ isDisconnecting = function (connection) { return connection.state === signalR.connectionState.disconnected; }, - + supportsKeepAlive = function (connection) { return connection._.keepAliveData.activated && connection.transport.supportsKeepAlive(connection); @@ -403,7 +403,7 @@ state: signalR.connectionState.disconnected, - clientProtocol: "1.4", + clientProtocol: "1.5", reconnectDelay: 2000, diff --git a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.common.js b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.common.js index c5114bd18e..f8e780c372 100644 --- a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.common.js +++ b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.common.js @@ -206,13 +206,14 @@ throw new Error("Query string property must be either a string or object."); }, - getUrl: function (connection, transport, reconnecting, poll) { + // BUG #2953: The url needs to be same otherwise it will cause a memory leak + getUrl: function (connection, transport, reconnecting, poll, ajaxPost) { /// Gets the url for making a GET based connect request var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, url = baseUrl + connection.appRelativeUrl, qs = "transport=" + transport; - if (connection.groupsToken) { + if (!ajaxPost && connection.groupsToken) { qs += "&groupsToken=" + window.encodeURIComponent(connection.groupsToken); } @@ -226,13 +227,17 @@ url += "/reconnect"; } - if (connection.messageId) { + if (!ajaxPost && connection.messageId) { qs += "&messageId=" + window.encodeURIComponent(connection.messageId); } } url += "?" + qs; url = transportLogic.prepareQueryString(connection, url); - url += "&tid=" + Math.floor(Math.random() * 11); + + if (!ajaxPost) { + url += "&tid=" + Math.floor(Math.random() * 11); + } + return url; }, diff --git a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.longPolling.js b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.longPolling.js index 142cffddba..26a3ec77f3 100644 --- a/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.longPolling.js +++ b/src/Microsoft.AspNet.SignalR.Client.JS/jquery.signalR.transports.longPolling.js @@ -90,7 +90,16 @@ connect = (messageId === null), reconnecting = !connect, polling = !raiseReconnect, - url = transportLogic.getUrl(instance, that.name, reconnecting, polling); + url = transportLogic.getUrl(instance, that.name, reconnecting, polling, true /* use Post for longPolling */), + postData = {}; + + if (instance.messageId) { + postData.messageId = instance.messageId; + } + + if (instance.groupsToken) { + postData.groupsToken = instance.groupsToken; + } // If we've disconnected during the time we've tried to re-instantiate the poll then stop. if (isDisconnecting(instance) === true) { @@ -105,6 +114,9 @@ } }, url: url, + type: "POST", + contentType: signalR._.defaultContentType, + data: postData, success: function (result) { var minData, delay = 0, diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ProtocolResolver.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ProtocolResolver.cs index 7e02be749e..c66cac40be 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ProtocolResolver.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/ProtocolResolver.cs @@ -12,7 +12,7 @@ public class ProtocolResolver private readonly Version _minimumDelayedStartVersion = new Version(1, 4); public ProtocolResolver() : - this(new Version(1, 2), new Version(1, 4)) + this(new Version(1, 2), new Version(1, 5)) { } diff --git a/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs b/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs index 4764db2117..44730d0b4f 100644 --- a/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs +++ b/src/Microsoft.AspNet.SignalR.Core/PersistentConnection.cs @@ -181,7 +181,7 @@ public Task ProcessRequest(IDictionary environment) /// Thrown if the transport wasn't specified. /// Thrown if the connection id wasn't specified. /// - public virtual Task ProcessRequest(HostContext context) + public virtual async Task ProcessRequest(HostContext context) { if (context == null) { @@ -195,18 +195,21 @@ public virtual Task ProcessRequest(HostContext context) if (IsNegotiationRequest(context.Request)) { - return ProcessNegotiationRequest(context); + await ProcessNegotiationRequest(context).PreserveCulture(); + return; } else if (IsPingRequest(context.Request)) { - return ProcessPingRequest(context); + await ProcessPingRequest(context).PreserveCulture(); + return; } Transport = GetTransport(context); if (Transport == null) { - return FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorUnknownTransport)); + await FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorUnknownTransport)).PreserveCulture(); + return; } string connectionToken = context.Request.QueryString["connectionToken"]; @@ -214,16 +217,18 @@ public virtual Task ProcessRequest(HostContext context) // If there's no connection id then this is a bad request if (String.IsNullOrEmpty(connectionToken)) { - return FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorMissingConnectionToken)); + await FailResponse(context.Response, String.Format(CultureInfo.CurrentCulture, Resources.Error_ProtocolErrorMissingConnectionToken)).PreserveCulture(); + return; } string connectionId; string message; int statusCode; - + if (!TryGetConnectionId(context, connectionToken, out connectionId, out message, out statusCode)) { - return FailResponse(context.Response, message, statusCode); + await FailResponse(context.Response, message, statusCode).PreserveCulture(); + return; } // Set the transport's connection id to the unprotected one @@ -232,8 +237,11 @@ public virtual Task ProcessRequest(HostContext context) // Get the user id from the request string userId = UserIdProvider.GetUserId(context.Request); + // Get the groups oken from the request + string groupsToken = await Transport.GetGroupsToken().PreserveCulture(); + IList signals = GetSignals(userId, connectionId); - IList groups = AppendGroupPrefixes(context, connectionId); + IList groups = AppendGroupPrefixes(context, connectionId, groupsToken); Connection connection = CreateConnection(connectionId, signals, groups); @@ -245,7 +253,8 @@ public virtual Task ProcessRequest(HostContext context) // because ProcessStartRequest calls OnConnected. if (IsStartRequest(context.Request)) { - return ProcessStartRequest(context, connectionId); + await ProcessStartRequest(context, connectionId).PreserveCulture(); + return; } Transport.Connected = () => @@ -277,7 +286,7 @@ public virtual Task ProcessRequest(HostContext context) } }; - return Transport.ProcessRequest(connection).OrEmpty().Catch(Counters.ErrorsAllTotal, Counters.ErrorsAllPerSec); + await Transport.ProcessRequest(connection).OrEmpty().Catch(Counters.ErrorsAllTotal, Counters.ErrorsAllPerSec).PreserveCulture(); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to catch any exception when unprotecting data.")] @@ -328,10 +337,8 @@ public virtual Task ProcessRequest(HostContext context) } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to prevent any failures in unprotecting")] - internal IList VerifyGroups(HostContext context, string connectionId) + internal IList VerifyGroups(string connectionId, string groupsToken) { - string groupsToken = context.Request.QueryString["groupsToken"]; - if (String.IsNullOrEmpty(groupsToken)) { return ListHelper.Empty; @@ -366,9 +373,9 @@ internal IList VerifyGroups(HostContext context, string connectionId) return JsonSerializer.Parse(groupsValue); } - private IList AppendGroupPrefixes(HostContext context, string connectionId) + private IList AppendGroupPrefixes(HostContext context, string connectionId, string groupsToken) { - return (from g in OnRejoiningGroups(context.Request, VerifyGroups(context, connectionId), connectionId) + return (from g in OnRejoiningGroups(context.Request, VerifyGroups(connectionId, groupsToken), connectionId) select GroupPrefix + g).ToList(); } diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs index 5469cca40f..f75d054dc0 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ForeverTransport.cs @@ -19,7 +19,6 @@ public abstract class ForeverTransport : TransportDisconnectBase, ITransport private readonly IPerformanceCounterManager _counters; private readonly JsonSerializer _jsonSerializer; - private string _lastMessageId; private IDisposable _busRegistration; internal RequestLifetime _transportLifetime; @@ -52,19 +51,6 @@ protected virtual int MaxMessages } } - protected string LastMessageId - { - get - { - if (_lastMessageId == null) - { - _lastMessageId = Context.Request.QueryString["messageId"]; - } - - return _lastMessageId; - } - } - protected JsonSerializer JsonSerializer { get { return _jsonSerializer; } @@ -92,32 +78,31 @@ protected virtual void OnSendingResponse(PersistentResponse response) internal Action BeforeReceive; internal Action AfterRequestEnd; - protected override void InitializePersistentState() + protected override async Task InitializePersistentState() { - base.InitializePersistentState(); + await base.InitializePersistentState().PreserveCulture(); // The _transportLifetime must be initialized after calling base.InitializePersistentState since // _transportLifetime depends on _requestLifetime. _transportLifetime = new RequestLifetime(this, _requestLifeTime); } - protected Task ProcessRequestCore(ITransportConnection connection) + protected async Task ProcessRequestCore(ITransportConnection connection) { Connection = connection; if (IsSendRequest) { - return ProcessSendRequest(); + await ProcessSendRequest().PreserveCulture(); } else if (IsAbortRequest) { - return Connection.Abort(ConnectionId); + await Connection.Abort(ConnectionId).PreserveCulture(); } else { - InitializePersistentState(); - - return ProcessReceiveRequest(connection); + await InitializePersistentState().PreserveCulture(); + await ProcessReceiveRequest(connection).PreserveCulture(); } } diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs index 772cca9030..eb61e5e9fa 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/ITransport.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.SignalR.Infrastructure; +using Microsoft.AspNet.SignalR.Hosting; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNet.SignalR.Transports { @@ -37,6 +39,13 @@ public interface ITransport /// string ConnectionId { get; set; } + /// + /// Get groupsToken in request over the transport. + /// + /// groupsToken in request + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This is for async.")] + Task GetGroupsToken(); + /// /// Processes the specified for this transport. /// diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs index 672be26416..b0a8a9bc9b 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/LongPollingTransport.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.SignalR.Json; using Microsoft.AspNet.SignalR.Tracing; using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNet.SignalR.Transports { @@ -94,6 +95,30 @@ protected override bool SuppressReconnect } } + protected override async Task InitializeMessageId() + { + _lastMessageId = Context.Request.QueryString["messageId"]; + + if (_lastMessageId == null) + { + var form = await Context.Request.ReadForm().PreserveCulture(); + _lastMessageId = form["messageId"]; + } + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This is for async.")] + public override async Task GetGroupsToken() + { + var groupsToken = Context.Request.QueryString["groupsToken"]; + + if (groupsToken == null) + { + var form = await Context.Request.ReadForm().PreserveCulture(); + groupsToken = form["groupsToken"]; + } + return groupsToken; + } + public override Task KeepAlive() { // Ensure delegate continues to use the C# Compiler static delegate caching optimization. @@ -250,7 +275,7 @@ private static Task PerformCompleteSend(object state) return PerformPartialSend(state); } - + private void AddTransportData(PersistentResponse response) { if (_configurationManager.LongPollDelay != TimeSpan.Zero) diff --git a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs index 64fcb07bed..28cefb7d33 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Transports/TransportDisconnectBase.cs @@ -26,6 +26,9 @@ public abstract class TransportDisconnectBase : ITrackingConnection private int _ended; private TransportConnectionStates _state; + [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields", Justification = "It can be set in any derived class.")] + protected string _lastMessageId; + internal static readonly Func _emptyTaskFunc = () => TaskAsyncHelper.Empty; // The TCS that completes when the task returned by PersistentConnection.OnConnected does. @@ -90,6 +93,26 @@ public string ConnectionId set; } + protected string LastMessageId + { + get + { + return _lastMessageId; + } + } + + protected virtual Task InitializeMessageId() + { + _lastMessageId = Context.Request.QueryString["messageId"]; + return TaskAsyncHelper.Empty; + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This is for async.")] + public virtual Task GetGroupsToken() + { + return TaskAsyncHelper.FromResult(Context.Request.QueryString["groupsToken"]); + } + public virtual TextWriter OutputWriter { get @@ -126,7 +149,7 @@ public virtual bool IsAlive { // If the CTS is tripped or the request has ended then the connection isn't alive return !( - CancellationToken.IsCancellationRequested || + CancellationToken.IsCancellationRequested || (_requestLifeTime != null && _requestLifeTime.Task.IsCompleted) || _lastWriteTask.IsCanceled || _lastWriteTask.IsFaulted @@ -357,7 +380,7 @@ protected virtual internal Task EnqueueOperation(Func writeAsync, return writeTask; } - protected virtual void InitializePersistentState() + protected virtual async Task InitializePersistentState() { _hostShutdownToken = _context.Environment.GetShutdownToken(); @@ -383,6 +406,8 @@ protected virtual void InitializePersistentState() ((HttpRequestLifeTime)state).Complete(); }, _requestLifeTime); + + await InitializeMessageId().PreserveCulture(); } private static void OnDisconnectError(AggregateException ex, object state) diff --git a/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/FunctionalTests/Hubs/HubGroupFacts.js b/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/FunctionalTests/Hubs/HubGroupFacts.js index 8d42f03859..2f659d9c98 100644 --- a/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/FunctionalTests/Hubs/HubGroupFacts.js +++ b/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/FunctionalTests/Hubs/HubGroupFacts.js @@ -87,7 +87,7 @@ testUtilities.runWithAllTransports(function (transport) { if (++pingCount === 2) { assert.comment("Pinged twice."); - + // Let sleep for 1 second to let any dups flow in (so we can fail) window.setTimeout(function () { end(); @@ -107,5 +107,44 @@ testUtilities.runWithAllTransports(function (transport) { }; }); + QUnit.asyncTimeoutTest(transport + ": group works after app domain restart when groupsToken just from client.", testUtilities.defaultTestTimeout * 3, function (end, assert, testName) { + var connection = testUtilities.createHubConnection(end, assert, testName), + groupChat = connection.createHubProxies().groupChat, + groupName = "group$&+,/:;=?@[]1"; + + groupChat.client.send = function (value) { + assert.ok(value === "hello", "Successful received message from group after reconnected"); + end(); + }; + + connection.reconnecting(function () { + assert.ok(true, "Connection is now attempting to reconnect"); + }); + + connection.reconnected(function () { + assert.ok(true, "Successfuly raised reconnected event "); + + // Workaround for bug#2642 on webSockets that requires to call server method after a short timeout in reconnected event + window.setTimeout(function () { + groupChat.server.send(groupName, "hello").done(function () { + assert.ok(true, "Successful send to group"); + }); + }, 30); + }); + + connection.start({ transport: transport }).done(function () { + assert.ok(true, "Connected"); + + groupChat.server.join(groupName).done(function () { + groupChat.server.triggerAppDomainRestart(); + }); + }); + + // Cleanup + return function () { + connection.stop(); + }; + }); + }); diff --git a/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/UnitTests/Common/UrlFacts.js b/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/UnitTests/Common/UrlFacts.js index c48b57183f..a04d0effd3 100644 --- a/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/UnitTests/Common/UrlFacts.js +++ b/tests/Microsoft.AspNet.SignalR.Client.JS.Tests/Tests/UnitTests/Common/UrlFacts.js @@ -8,7 +8,7 @@ QUnit.test("getUrl handles groupsToken correctly.", function () { // Every unsafe character group name. connection.groupsToken = '$&+,/:;=?@ "<>#%{}|\^[]`'; - $.each(testUtilities.transportNames, function () { + $.each(["webSocket", "serverSentEvents", "foreverFrame"], function () { var transport = this.toString(); url = $.signalR.transports._logic.getUrl(connection, transport, true); @@ -115,7 +115,7 @@ QUnit.test("getUrl handles messageId correctly", function () { // Every unsafe character name. connection.messageId = '$&+,/:;=?@ "<>#%{}|\^[]`'; - $.each(testUtilities.transportNames, function () { + $.each(["webSocket", "serverSentEvents", "foreverFrame"], function () { var transport = this.toString(); url = $.signalR.transports._logic.getUrl(connection, transport, true); diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Hubs/ChatWithGroups.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Hubs/ChatWithGroups.cs index 69ae1b94b3..4b90b3c378 100644 --- a/tests/Microsoft.AspNet.SignalR.Tests.Common/Hubs/ChatWithGroups.cs +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Hubs/ChatWithGroups.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Web; using Microsoft.AspNet.SignalR.Hubs; namespace Microsoft.AspNet.SignalR.Tests.Common.Hubs @@ -24,5 +21,10 @@ public void Leave(string group) { Groups.Remove(Context.ConnectionId, group); } + + public void TriggerAppDomainRestart() + { + HttpRuntime.UnloadAppDomain(); + } } } diff --git a/tests/Microsoft.AspNet.SignalR.Tests/PersistentConnectionFacts.cs b/tests/Microsoft.AspNet.SignalR.Tests/PersistentConnectionFacts.cs index 3ff5a61ceb..2283ad2535 100644 --- a/tests/Microsoft.AspNet.SignalR.Tests/PersistentConnectionFacts.cs +++ b/tests/Microsoft.AspNet.SignalR.Tests/PersistentConnectionFacts.cs @@ -7,6 +7,7 @@ using Microsoft.AspNet.SignalR.Tests.Common.Infrastructure; using Moq; using Xunit; +using Microsoft.AspNet.SignalR.Tests.Utilities; namespace Microsoft.AspNet.SignalR.Tests { @@ -18,14 +19,22 @@ public class ProcessRequest public void NullContextThrows() { var connection = new Mock() { CallBase = true }; - Assert.Throws(() => connection.Object.ProcessRequest((HostContext)null)); + + TestUtilities.AssertUnwrappedException(() => + { + connection.Object.ProcessRequest((HostContext)null).Wait(); + }); } [Fact] public void UninitializedThrows() { var connection = new Mock() { CallBase = true }; - Assert.Throws(() => connection.Object.ProcessRequest(new HostContext(null, null))); + + TestUtilities.AssertUnwrappedException(() => + { + connection.Object.ProcessRequest(new HostContext(null, null)).Wait(); + }); } [Fact] @@ -137,7 +146,7 @@ private static IList DoVerifyGroups(string groupsToken, string connectio var context = new HostContext(req.Object, null); connection.Object.Initialize(dr); - return connection.Object.VerifyGroups(context, connectionId); + return connection.Object.VerifyGroups(connectionId, groupsToken); } } diff --git a/tests/Microsoft.AspNet.SignalR.Tests/Server/Transports/LongPollingTransportFacts.cs b/tests/Microsoft.AspNet.SignalR.Tests/Server/Transports/LongPollingTransportFacts.cs index 56eeb28b71..fe3c05abb0 100644 --- a/tests/Microsoft.AspNet.SignalR.Tests/Server/Transports/LongPollingTransportFacts.cs +++ b/tests/Microsoft.AspNet.SignalR.Tests/Server/Transports/LongPollingTransportFacts.cs @@ -82,6 +82,8 @@ public static TestLongPollingTransport Create(string requestPath) { var request = new Mock(); request.Setup(m => m.QueryString).Returns(new NameValueCollectionWrapper()); + request.Setup(m => m.ReadForm()) + .Returns(TaskAsyncHelper.FromResult((INameValueCollection)new NameValueCollectionWrapper())); request.Setup(m => m.LocalPath).Returns(requestPath); var response = new Mock();