Merge pull request #3373 from marek-safar/net-4.6.2
[mono.git] / mcs / class / referencesource / System.Web / WebSocketPipeline.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="WebSocketContext.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>                                                                
5 //------------------------------------------------------------------------------
6
7 namespace System.Web {
8     using System;
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;
18
19     // Responsible for kicking off the WebSocket pipeline at the end of an ASP.NET request
20
21     internal sealed class WebSocketPipeline : IDisposable, ISyncContext {
22
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;
28
29         public WebSocketPipeline(RootedObjects root, HttpContext httpContext, Func<AspNetWebSocketContext, Task> userFunc, string subProtocol) {
30             _root = root;
31             _httpContext = httpContext;
32             _userFunc = userFunc;
33             _subProtocol = subProtocol;
34         }
35
36         public void Dispose() {
37             // disposal not currently needed
38         }
39
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();
43
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();
48
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);
52         }
53
54         private ExceptionDispatchInfo DoFlush() {
55             // See comments in ProcessRequestImplAsync() for why this method returns an ExceptionDispatchInfo
56             // rather than allowing exceptions to propagate out.
57
58             try {
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
61                 return null;
62             }
63             catch (Exception ex) {
64                 return ExceptionDispatchInfo.Capture(ex);
65             }
66         }
67
68         private async Task<AspNetWebSocket> ProcessRequestImplAsync() {
69             AspNetWebSocket webSocket = null;
70
71             try {
72                 // SendResponse and other asynchronous notifications cannot be process by ASP.NET after this point.
73                 _root.WorkerRequest.SuppressSendResponseNotifications();
74
75                 // A flush is necessary to activate the WebSocket module so that we can get its pointer.
76                 //
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();
83
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);
90
91                 // slim down the HttpContext as much as possible to allow the GC to reclaim memory
92                 _httpContext.CompleteTransitionToWebSocket();
93
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;
97
98                 bool webSocketRequestSucceeded = false;
99                 try {
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);
102
103                     // bump up the total count (the currently-executing count is recorded separately)
104                     PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_TOTAL_WEBSOCKETS);
105
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.
108                     Task task = null;
109                     syncContext.Send(_ => {
110                         task = _userFunc(new AspNetWebSocketContextImpl(new HttpContextWrapper(_httpContext), _root.WorkerRequest, webSocket));
111                     }, null);
112
113                     // Was there an exception from user code? If so, rethrow (which logs).
114                     ExceptionDispatchInfo exception = syncContext.ExceptionDispatchInfo;
115                     if (exception != null) {
116                         exception.Throw();
117                     }
118
119                     _userFunc = null;
120                     await task.ConfigureAwait(continueOnCapturedContext: false);
121
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();
127                     }
128
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.
134
135                     if (webSocket.State != WebSocketState.Aborted) {
136                         webSocketRequestSucceeded = true;
137                         PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_SUCCEEDED_WEBSOCKETS);
138                     }
139                 }
140                 finally {
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);
145
146                     if (!webSocketRequestSucceeded) {
147                         PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_FAILED_WEBSOCKETS);
148                     }
149                 }
150             }
151             catch (Exception ex) {
152                 // don't let the exception propagate upward; just log it instead
153                 WebBaseEvent.RaiseRuntimeError(ex, null);
154             }
155
156             return webSocket;
157         }
158
159         // consumed by AppVerifier when it is enabled
160         HttpContext ISyncContext.HttpContext {
161             get {
162                 // if processing is finished, this ISyncContext is no longer logically associated with an HttpContext, so return null
163                 return (_isProcessingComplete) ? null : _httpContext;
164             }
165         }
166
167         ISyncContextLock ISyncContext.Enter() {
168             // Restores impersonation, Culture, etc.
169             ThreadContext threadContext = new ThreadContext(_httpContext);
170             threadContext.AssociateWithCurrentThread(_httpContext.UsesImpersonation);
171             return threadContext;
172         }
173
174     }
175 }