1 //------------------------------------------------------------------------------
2 // <copyright file="WebSocketContext.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
9 using System.Diagnostics.CodeAnalysis;
10 using System.Net.WebSockets;
11 using System.Runtime.ExceptionServices;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using System.Web.Hosting;
15 using System.Web.Management;
16 using System.Web.Util;
17 using System.Web.WebSockets;
19 // Responsible for kicking off the WebSocket pipeline at the end of an ASP.NET request
21 internal sealed class WebSocketPipeline : IDisposable, ISyncContext {
23 private readonly RootedObjects _root;
24 private readonly HttpContext _httpContext;
25 private volatile bool _isProcessingComplete;
26 private Func<AspNetWebSocketContext, Task> _userFunc;
27 private readonly string _subProtocol;
29 public WebSocketPipeline(RootedObjects root, HttpContext httpContext, Func<AspNetWebSocketContext, Task> userFunc, string subProtocol) {
31 _httpContext = httpContext;
33 _subProtocol = subProtocol;
36 public void Dispose() {
37 // disposal not currently needed
40 [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Web.Hosting.UnsafeIISMethods.MgdPostCompletion(System.IntPtr,System.Web.RequestNotificationStatus)", Justification = @"This will never return an error HRESULT.")]
41 public void ProcessRequest() {
42 Task<AspNetWebSocket> webSocketTask = ProcessRequestImplAsync();
44 // If 'webSocketTask' contains a non-null Result, this is our last chance to ensure that we
45 // have completed all pending IO. Otherwise we run the risk of iiswsock.dll making a reverse
46 // p/invoke call into our managed layer and touching objects that have already been GCed.
47 Task abortTask = webSocketTask.ContinueWith(task => (task.Result != null) ? ((AspNetWebSocket)task.Result).AbortAsync() : (Task)null, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
49 // Once all pending IOs are complete, we can progress the IIS state machine and finish the request.
50 // Execute synchronously since it's very short-running (posts to the native ThreadPool).
51 abortTask.ContinueWith(_ => UnsafeIISMethods.MgdPostCompletion(_root.WorkerRequest.RequestContext, RequestNotificationStatus.Continue), TaskContinuationOptions.ExecuteSynchronously);
54 private ExceptionDispatchInfo DoFlush() {
55 // See comments in ProcessRequestImplAsync() for why this method returns an ExceptionDispatchInfo
56 // rather than allowing exceptions to propagate out.
59 _root.WorkerRequest.FlushResponse(finalFlush: true); // pushes buffers to IIS; completes synchronously
60 _root.WorkerRequest.ExplicitFlush(); // signals IIS to push its buffers to the network
63 catch (Exception ex) {
64 return ExceptionDispatchInfo.Capture(ex);
68 private async Task<AspNetWebSocket> ProcessRequestImplAsync() {
69 AspNetWebSocket webSocket = null;
72 // SendResponse and other asynchronous notifications cannot be process by ASP.NET after this point.
73 _root.WorkerRequest.SuppressSendResponseNotifications();
75 // A flush is necessary to activate the WebSocket module so that we can get its pointer.
77 // DevDiv #401948: We can't allow a flush failure to propagate out, otherwise the rest of
78 // this method doesn't run, which could leak resources (by not invoking the user callback)
79 // or cause weird behavior (by not calling CompleteTransitionToWebSocket, which could corrupt
80 // server state). If the flush fails, we'll wait to propagate the exception until a safe
81 // point later in this method.
82 ExceptionDispatchInfo flushExceptionDispatchInfo = DoFlush();
84 // Create the AspNetWebSocket. There's a chance that the client disconnected before we
85 // hit this code. If this is the case, we'll pass a null WebSocketPipe to the
86 // AspNetWebSocket ctor, which immediately sets the socket into an aborted state.
87 UnmanagedWebSocketContext unmanagedWebSocketContext = _root.WorkerRequest.GetWebSocketContext();
88 WebSocketPipe pipe = (unmanagedWebSocketContext != null) ? new WebSocketPipe(unmanagedWebSocketContext, PerfCounters.Instance) : null;
89 webSocket = new AspNetWebSocket(pipe, _subProtocol);
91 // slim down the HttpContext as much as possible to allow the GC to reclaim memory
92 _httpContext.CompleteTransitionToWebSocket();
94 // always install a new SynchronizationContext, even if the user is running in legacy SynchronizationContext mode
95 AspNetSynchronizationContext syncContext = new AspNetSynchronizationContext(this);
96 _httpContext.SyncContext = syncContext;
98 bool webSocketRequestSucceeded = false;
100 // need to keep track of this in the manager so that we can abort if it the AppDomain goes down
101 AspNetWebSocketManager.Current.Add(webSocket);
103 // bump up the total count (the currently-executing count is recorded separately)
104 PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_TOTAL_WEBSOCKETS);
106 // Release the reference to the user delegate (which might just be a simple initialization routine) so that
107 // the GC can claim it. The only thing that needs to remain alive is the Task itself, which we're referencing.
109 syncContext.Send(_ => {
110 task = _userFunc(new AspNetWebSocketContextImpl(new HttpContextWrapper(_httpContext), _root.WorkerRequest, webSocket));
113 // Was there an exception from user code? If so, rethrow (which logs).
114 ExceptionDispatchInfo exception = syncContext.ExceptionDispatchInfo;
115 if (exception != null) {
120 await task.ConfigureAwait(continueOnCapturedContext: false);
122 // Was there an exception from the earlier call to DoFlush? If so, rethrow (which logs).
123 // This needs to occur after the user's callback finishes, otherwise ASP.NET could try
124 // to complete the request while the callback is still accessing it.
125 if (flushExceptionDispatchInfo != null) {
126 flushExceptionDispatchInfo.Throw();
129 // Any final state except Aborted is marked as 'success'.
130 // It's possible execution never reaches this point, e.g. if the user's
131 // callback throws an exception. In that case, 'webSocketRequestSucceeded'
132 // will keep its default value of false, and the performance counter
133 // will mark this request as having failed.
135 if (webSocket.State != WebSocketState.Aborted) {
136 webSocketRequestSucceeded = true;
137 PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_SUCCEEDED_WEBSOCKETS);
141 // we need to make sure the user can't call the WebSocket any more after this point
142 _isProcessingComplete = true;
143 webSocket.DisposeInternal();
144 AspNetWebSocketManager.Current.Remove(webSocket);
146 if (!webSocketRequestSucceeded) {
147 PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_FAILED_WEBSOCKETS);
151 catch (Exception ex) {
152 // don't let the exception propagate upward; just log it instead
153 WebBaseEvent.RaiseRuntimeError(ex, null);
159 // consumed by AppVerifier when it is enabled
160 HttpContext ISyncContext.HttpContext {
162 // if processing is finished, this ISyncContext is no longer logically associated with an HttpContext, so return null
163 return (_isProcessingComplete) ? null : _httpContext;
167 ISyncContextLock ISyncContext.Enter() {
168 // Restores impersonation, Culture, etc.
169 ThreadContext threadContext = new ThreadContext(_httpContext);
170 threadContext.AssociateWithCurrentThread(_httpContext.UsesImpersonation);
171 return threadContext;