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