appium-webdriveragent 16.7.2 → 16.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +0 -10
- package/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +90 -0
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +4 -1
- package/WebDriverAgentLib/Commands/FBScreenshotCommands.m +2 -2
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +6 -5
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.h +25 -1
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +278 -92
- package/WebDriverAgentLib/Routing/FBRoute.h +9 -0
- package/WebDriverAgentLib/Routing/FBRoute.m +9 -0
- package/WebDriverAgentLib/Routing/FBSession.h +13 -0
- package/WebDriverAgentLib/Routing/FBSession.m +153 -22
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +2 -1
- package/WebDriverAgentLib/Routing/FBWebServer.m +26 -1
- package/WebDriverAgentLib/Routing/RouteResponse.m +1 -1
- package/WebDriverAgentLib/Utilities/FBConfiguration.h +11 -0
- package/WebDriverAgentLib/Utilities/FBConfiguration.m +8 -10
- package/WebDriverAgentLib/Utilities/FBSettings.h +1 -0
- package/WebDriverAgentLib/Utilities/FBSettings.m +1 -0
- package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +7 -0
- package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +9 -0
- package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +26 -18
- package/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +10 -7
- package/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +36 -15
- package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +4 -3
- package/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +0 -1
- package/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +34 -0
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
35
35
|
@property (nonatomic, strong) NSRegularExpression *regex;
|
|
36
36
|
@property (nonatomic, copy, nullable) NSArray<NSString *> *keys;
|
|
37
37
|
@property (nonatomic, copy) void (^block)(RouteRequest *request, RouteResponse *response);
|
|
38
|
+
@property (nonatomic, assign) BOOL isStandalone;
|
|
38
39
|
@end
|
|
39
40
|
|
|
40
41
|
@implementation FBHTTPRoute
|
|
@@ -55,6 +56,25 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
55
56
|
@end
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
// One dispatched-but-not-yet-answered request. Default (pointer) identity, so two pipelined
|
|
60
|
+
// requests sharing a connection are never conflated into a single tracked entry.
|
|
61
|
+
@interface FBPendingRequest : NSObject
|
|
62
|
+
@property (nonatomic, strong, readonly) nw_connection_t client;
|
|
63
|
+
@end
|
|
64
|
+
|
|
65
|
+
@implementation FBPendingRequest
|
|
66
|
+
|
|
67
|
+
- (instancetype)initWithClient:(nw_connection_t)client
|
|
68
|
+
{
|
|
69
|
+
if ((self = [super init])) {
|
|
70
|
+
_client = client;
|
|
71
|
+
}
|
|
72
|
+
return self;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@end
|
|
76
|
+
|
|
77
|
+
|
|
58
78
|
@interface FBHTTPServer () <FBTCPSocketDelegate>
|
|
59
79
|
|
|
60
80
|
@property (nonatomic, nullable, strong) FBTCPSocket *socket;
|
|
@@ -67,6 +87,20 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
67
87
|
// Per-client cache of the already-parsed request line + headers while its body is still
|
|
68
88
|
// arriving; nil while a client's next unread bytes start with an unparsed header block.
|
|
69
89
|
@property (nonatomic, strong) NSMapTable<id, FBPendingHTTPRequestHeader *> *pendingRequestHeaders;
|
|
90
|
+
// All buffer access - appending new data and -processBufferForClient:'s unlocked parse - is
|
|
91
|
+
// funneled through this one serial queue, so appends can never race a parse.
|
|
92
|
+
@property (nonatomic, strong) dispatch_queue_t bufferProcessingQueue;
|
|
93
|
+
// Connections with a request parsed off the buffer but not yet answered. Blocks
|
|
94
|
+
// -processBufferForClient: from starting the next pipelined request, so responses on one
|
|
95
|
+
// connection can't be written out of order. Guarded by @synchronized(self.connectionBuffers).
|
|
96
|
+
@property (nonatomic, strong) NSMutableSet *connectionsAwaitingResponse;
|
|
97
|
+
// Keyed by "METHOD path" - requests waiting on an already in-flight standalone request for that
|
|
98
|
+
// endpoint. Guarded by @synchronized(self.standaloneWaiters).
|
|
99
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<FBPendingRequest *> *> *standaloneWaiters;
|
|
100
|
+
// Keyed by the "sessionID" path param - requests currently queued or executing for that session,
|
|
101
|
+
// standalone or not (except DELETE /session itself - see -dispatchMethod:). See
|
|
102
|
+
// -abandonPendingRequestsForSessionID:. Guarded by @synchronized(self.pendingSessionRequests).
|
|
103
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<FBPendingRequest *> *> *pendingSessionRequests;
|
|
70
104
|
|
|
71
105
|
@end
|
|
72
106
|
|
|
@@ -81,6 +115,10 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
81
115
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
82
116
|
_pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
83
117
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
118
|
+
_bufferProcessingQueue = dispatch_queue_create("com.facebook.wda.http.bufferProcessing", DISPATCH_QUEUE_SERIAL);
|
|
119
|
+
_connectionsAwaitingResponse = [NSMutableSet set];
|
|
120
|
+
_standaloneWaiters = [NSMutableDictionary dictionary];
|
|
121
|
+
_pendingSessionRequests = [NSMutableDictionary dictionary];
|
|
84
122
|
}
|
|
85
123
|
return self;
|
|
86
124
|
}
|
|
@@ -107,8 +145,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
107
145
|
FBHTTPRoute *route = [FBHTTPRoute new];
|
|
108
146
|
NSMutableArray<NSString *> *keys = [NSMutableArray array];
|
|
109
147
|
|
|
110
|
-
// Escape regex-significant characters before substituting :param placeholders
|
|
111
|
-
// RoutingHTTPServer.m used to.
|
|
148
|
+
// Escape regex-significant characters before substituting :param placeholders.
|
|
112
149
|
NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]"
|
|
113
150
|
options:(NSRegularExpressionOptions)0
|
|
114
151
|
error:nil];
|
|
@@ -157,10 +194,19 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
157
194
|
- (void)handleMethod:(NSString *)method
|
|
158
195
|
withPath:(NSString *)path
|
|
159
196
|
block:(void (^)(RouteRequest *request, RouteResponse *response))block
|
|
197
|
+
{
|
|
198
|
+
[self handleMethod:method withPath:path standalone:NO block:block];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
- (void)handleMethod:(NSString *)method
|
|
202
|
+
withPath:(NSString *)path
|
|
203
|
+
standalone:(BOOL)standalone
|
|
204
|
+
block:(void (^)(RouteRequest *request, RouteResponse *response))block
|
|
160
205
|
{
|
|
161
206
|
FBHTTPRoute *route = [self compiledRouteWithPath:path];
|
|
162
207
|
route.verb = method.uppercaseString;
|
|
163
208
|
route.block = block;
|
|
209
|
+
route.isStandalone = standalone;
|
|
164
210
|
[self.routes addObject:route];
|
|
165
211
|
}
|
|
166
212
|
|
|
@@ -191,6 +237,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
191
237
|
@synchronized (self.connectionBuffers) {
|
|
192
238
|
[self.connectionBuffers removeAllObjects];
|
|
193
239
|
[self.pendingRequestHeaders removeAllObjects];
|
|
240
|
+
[self.connectionsAwaitingResponse removeAllObjects];
|
|
194
241
|
}
|
|
195
242
|
_isRunning = NO;
|
|
196
243
|
}
|
|
@@ -209,123 +256,136 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
209
256
|
@synchronized (self.connectionBuffers) {
|
|
210
257
|
[self.connectionBuffers removeObjectForKey:client];
|
|
211
258
|
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
259
|
+
[self.connectionsAwaitingResponse removeObject:client];
|
|
212
260
|
}
|
|
213
261
|
}
|
|
214
262
|
|
|
215
263
|
- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data
|
|
216
264
|
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
265
|
+
// The append itself, not just the parse, must run on bufferProcessingQueue: otherwise a receive
|
|
266
|
+
// callback here could still mutate the buffer while -processBufferForClient: is reading it
|
|
267
|
+
// unlocked on that queue.
|
|
268
|
+
__weak typeof(self) weakSelf = self;
|
|
269
|
+
dispatch_async(self.bufferProcessingQueue, ^{
|
|
270
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
271
|
+
if (nil == strongSelf) {
|
|
221
272
|
return;
|
|
222
273
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
274
|
+
@synchronized (strongSelf.connectionBuffers) {
|
|
275
|
+
NSMutableData *buffer = [strongSelf.connectionBuffers objectForKey:client];
|
|
276
|
+
if (nil == buffer) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
[buffer appendData:data];
|
|
280
|
+
}
|
|
281
|
+
[strongSelf processBufferForClient:client];
|
|
282
|
+
});
|
|
226
283
|
}
|
|
227
284
|
|
|
228
285
|
#pragma mark - HTTP parsing
|
|
229
286
|
|
|
287
|
+
// Parses and dispatches at most one request per call; a connection with one already in flight is
|
|
288
|
+
// left alone (see -connectionsAwaitingResponse) until its response is written.
|
|
230
289
|
- (void)processBufferForClient:(nw_connection_t)client
|
|
231
290
|
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (nil == buffer) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
pending = [self.pendingRequestHeaders objectForKey:client];
|
|
291
|
+
NSMutableData *buffer;
|
|
292
|
+
FBPendingHTTPRequestHeader *pending;
|
|
293
|
+
@synchronized (self.connectionBuffers) {
|
|
294
|
+
if ([self.connectionsAwaitingResponse containsObject:client]) {
|
|
295
|
+
return;
|
|
241
296
|
}
|
|
297
|
+
buffer = [self.connectionBuffers objectForKey:client];
|
|
298
|
+
if (nil == buffer) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
pending = [self.pendingRequestHeaders objectForKey:client];
|
|
302
|
+
}
|
|
242
303
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
304
|
+
if (nil == pending) {
|
|
305
|
+
NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
|
|
306
|
+
if (NSNotFound == headerEndRange.location) {
|
|
307
|
+
// Wait for the rest of the header block to arrive.
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
249
310
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
311
|
+
NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)];
|
|
312
|
+
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
|
313
|
+
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
|
314
|
+
if (lines.count < 1) {
|
|
315
|
+
[self respondBadRequestToClient:client];
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
257
318
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
319
|
+
NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
|
|
320
|
+
if (requestLineParts.count < 2) {
|
|
321
|
+
[self respondBadRequestToClient:client];
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
263
324
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
NSString *name = [line substringToIndex:colonRange.location];
|
|
272
|
-
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
273
|
-
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
274
|
-
requestHeaders[name.lowercaseString] = value;
|
|
325
|
+
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
326
|
+
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
327
|
+
NSString *line = lines[i];
|
|
328
|
+
NSRange colonRange = [line rangeOfString:@":"];
|
|
329
|
+
if (NSNotFound == colonRange.location) {
|
|
330
|
+
continue;
|
|
275
331
|
}
|
|
332
|
+
NSString *name = [line substringToIndex:colonRange.location];
|
|
333
|
+
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
334
|
+
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
335
|
+
requestHeaders[name.lowercaseString] = value;
|
|
336
|
+
}
|
|
276
337
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
338
|
+
NSString *transferEncoding = requestHeaders[@"transfer-encoding"];
|
|
339
|
+
if (transferEncoding.length > 0) {
|
|
340
|
+
// No transfer decoder is implemented at all, so any encoding (chunked or otherwise -
|
|
341
|
+
// including a value only introduced by a duplicate header overwriting "chunked" above)
|
|
342
|
+
// is rejected rather than risking the body being misread as empty and desyncing the rest
|
|
343
|
+
// of the connection's request stream.
|
|
344
|
+
RouteResponse *notImplemented = [RouteResponse new];
|
|
345
|
+
id<FBResponsePayload> notImplementedPayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Transfer-Encoding is not supported"
|
|
285
346
|
traceback:nil]);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
|
|
292
|
-
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
293
|
-
// Mirrors CocoaHTTPServer's maxRequestBodySize enforcement. Closes the connection after
|
|
294
|
-
// responding, since the rest of the oversized body is still incoming.
|
|
295
|
-
RouteResponse *tooLarge = [RouteResponse new];
|
|
296
|
-
id<FBResponsePayload> tooLargePayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Request Entity Too Large"
|
|
297
|
-
traceback:nil]);
|
|
298
|
-
[tooLargePayload dispatchWithResponse:tooLarge];
|
|
299
|
-
[self failClient:client withResponse:tooLarge];
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
pending = [FBPendingHTTPRequestHeader new];
|
|
304
|
-
pending.method = requestLineParts[0].uppercaseString;
|
|
305
|
-
pending.pathAndQuery = requestLineParts[1];
|
|
306
|
-
pending.bodyStart = headerEndRange.location + headerEndRange.length;
|
|
307
|
-
pending.contentLength = contentLength;
|
|
308
|
-
@synchronized (self.connectionBuffers) {
|
|
309
|
-
[self.pendingRequestHeaders setObject:pending forKey:client];
|
|
310
|
-
}
|
|
347
|
+
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
348
|
+
[self failClient:client withResponse:notImplemented];
|
|
349
|
+
return;
|
|
311
350
|
}
|
|
312
351
|
|
|
313
|
-
NSUInteger
|
|
314
|
-
if (
|
|
315
|
-
//
|
|
316
|
-
|
|
352
|
+
NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
|
|
353
|
+
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
354
|
+
// Closes the connection after responding, since the rest of the oversized body is still incoming.
|
|
355
|
+
RouteResponse *tooLarge = [RouteResponse new];
|
|
356
|
+
id<FBResponsePayload> tooLargePayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request body exceeds the configured size limit"
|
|
357
|
+
traceback:nil]);
|
|
358
|
+
[tooLargePayload dispatchWithResponse:tooLarge];
|
|
359
|
+
[self failClient:client withResponse:tooLarge];
|
|
317
360
|
return;
|
|
318
361
|
}
|
|
319
362
|
|
|
320
|
-
|
|
321
|
-
|
|
363
|
+
pending = [FBPendingHTTPRequestHeader new];
|
|
364
|
+
pending.method = requestLineParts[0].uppercaseString;
|
|
365
|
+
pending.pathAndQuery = requestLineParts[1];
|
|
366
|
+
pending.bodyStart = headerEndRange.location + headerEndRange.length;
|
|
367
|
+
pending.contentLength = contentLength;
|
|
322
368
|
@synchronized (self.connectionBuffers) {
|
|
323
|
-
[
|
|
324
|
-
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
369
|
+
[self.pendingRequestHeaders setObject:pending forKey:client];
|
|
325
370
|
}
|
|
371
|
+
}
|
|
326
372
|
|
|
327
|
-
|
|
373
|
+
NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength;
|
|
374
|
+
if (buffer.length < totalRequestLength) {
|
|
375
|
+
// Wait for the rest of the body to arrive - the parsed header stays cached above, so this
|
|
376
|
+
// doesn't re-scan/re-parse the header block on every subsequently arriving chunk.
|
|
377
|
+
return;
|
|
328
378
|
}
|
|
379
|
+
|
|
380
|
+
NSData *body = pending.contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(pending.bodyStart, pending.contentLength)] : [NSData data];
|
|
381
|
+
|
|
382
|
+
@synchronized (self.connectionBuffers) {
|
|
383
|
+
[buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0];
|
|
384
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
385
|
+
[self.connectionsAwaitingResponse addObject:client];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
[self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client];
|
|
329
389
|
}
|
|
330
390
|
|
|
331
391
|
// Removes the client's buffered state and responds with a closing error response. Removing the
|
|
@@ -344,8 +404,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
344
404
|
- (void)respondBadRequestToClient:(nw_connection_t)client
|
|
345
405
|
{
|
|
346
406
|
RouteResponse *badRequest = [RouteResponse new];
|
|
347
|
-
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus
|
|
348
|
-
|
|
407
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request could not be parsed as valid HTTP"
|
|
408
|
+
traceback:nil]);
|
|
349
409
|
[payload dispatchWithResponse:badRequest];
|
|
350
410
|
[self failClient:client withResponse:badRequest];
|
|
351
411
|
}
|
|
@@ -388,9 +448,29 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
388
448
|
RouteResponse *response = [RouteResponse new];
|
|
389
449
|
[self applyDefaultHeadersToResponse:response];
|
|
390
450
|
|
|
451
|
+
NSString *sessionID = params[@"sessionID"];
|
|
452
|
+
if (route.isStandalone) {
|
|
453
|
+
// DELETE triggers -abandonPendingRequestsForSessionID: itself; tracking its own request
|
|
454
|
+
// would make it abandon itself and write a response twice.
|
|
455
|
+
NSString *trackedSessionID = [route.verb isEqualToString:@"DELETE"] ? nil : sessionID;
|
|
456
|
+
[self dispatchStandaloneRoute:route request:request response:response client:client method:method pathAndQuery:pathAndQuery sessionID:trackedSessionID];
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
FBPendingRequest *pendingRequest = nil;
|
|
461
|
+
if (nil != sessionID) {
|
|
462
|
+
pendingRequest = [[FBPendingRequest alloc] initWithClient:client];
|
|
463
|
+
[self trackPendingRequest:pendingRequest forSessionID:sessionID];
|
|
464
|
+
}
|
|
465
|
+
|
|
391
466
|
void (^invoke)(void) = ^{
|
|
392
467
|
route.block(request, response);
|
|
393
|
-
|
|
468
|
+
// Whoever untracks `pendingRequest` first "wins" and gets to respond - either this normal
|
|
469
|
+
// completion, or -abandonPendingRequestsForSessionID: on another thread.
|
|
470
|
+
BOOL shouldRespond = (nil == pendingRequest) || [self untrackPendingRequest:pendingRequest forSessionID:sessionID];
|
|
471
|
+
if (shouldRespond) {
|
|
472
|
+
[self writeResponse:response toClient:client];
|
|
473
|
+
}
|
|
394
474
|
};
|
|
395
475
|
dispatch_queue_t routeQueue = self.routeQueue;
|
|
396
476
|
if (routeQueue) {
|
|
@@ -409,6 +489,103 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
409
489
|
[self writeResponse:notFound toClient:client];
|
|
410
490
|
}
|
|
411
491
|
|
|
492
|
+
#pragma mark - Session-scoped request cancellation
|
|
493
|
+
|
|
494
|
+
- (void)trackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID
|
|
495
|
+
{
|
|
496
|
+
@synchronized (self.pendingSessionRequests) {
|
|
497
|
+
NSMutableSet<FBPendingRequest *> *pendingRequests = self.pendingSessionRequests[sessionID];
|
|
498
|
+
if (nil == pendingRequests) {
|
|
499
|
+
pendingRequests = [NSMutableSet set];
|
|
500
|
+
self.pendingSessionRequests[sessionID] = pendingRequests;
|
|
501
|
+
}
|
|
502
|
+
[pendingRequests addObject:pendingRequest];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Returns YES if this caller won the race to respond, vs. -abandonPendingRequestsForSessionID:
|
|
507
|
+
// already having claimed `pendingRequest` on another thread.
|
|
508
|
+
- (BOOL)untrackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID
|
|
509
|
+
{
|
|
510
|
+
@synchronized (self.pendingSessionRequests) {
|
|
511
|
+
NSMutableSet<FBPendingRequest *> *pendingRequests = self.pendingSessionRequests[sessionID];
|
|
512
|
+
BOOL wasPending = [pendingRequests containsObject:pendingRequest];
|
|
513
|
+
if (wasPending) {
|
|
514
|
+
[pendingRequests removeObject:pendingRequest];
|
|
515
|
+
if (0 == pendingRequests.count) {
|
|
516
|
+
[self.pendingSessionRequests removeObjectForKey:sessionID];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return wasPending;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
- (void)abandonPendingRequestsForSessionID:(NSString *)sessionID withResponse:(RouteResponse *)response
|
|
524
|
+
{
|
|
525
|
+
NSSet<FBPendingRequest *> *pendingRequests;
|
|
526
|
+
@synchronized (self.pendingSessionRequests) {
|
|
527
|
+
pendingRequests = [self.pendingSessionRequests[sessionID] copy];
|
|
528
|
+
[self.pendingSessionRequests removeObjectForKey:sessionID];
|
|
529
|
+
}
|
|
530
|
+
for (FBPendingRequest *pendingRequest in pendingRequests) {
|
|
531
|
+
[self writeResponse:response toClient:pendingRequest.client];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#pragma mark - Standalone route dispatch
|
|
536
|
+
|
|
537
|
+
- (void)dispatchStandaloneRoute:(FBHTTPRoute *)route
|
|
538
|
+
request:(RouteRequest *)request
|
|
539
|
+
response:(RouteResponse *)response
|
|
540
|
+
client:(nw_connection_t)client
|
|
541
|
+
method:(NSString *)method
|
|
542
|
+
pathAndQuery:(NSString *)pathAndQuery
|
|
543
|
+
sessionID:(nullable NSString *)sessionID
|
|
544
|
+
{
|
|
545
|
+
// Includes the query string so requests with different params are never coalesced together.
|
|
546
|
+
NSString *key = [NSString stringWithFormat:@"%@ %@", method, pathAndQuery];
|
|
547
|
+
FBPendingRequest *waiter = [[FBPendingRequest alloc] initWithClient:client];
|
|
548
|
+
if (nil != sessionID) {
|
|
549
|
+
[self trackPendingRequest:waiter forSessionID:sessionID];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
BOOL isInFlight = NO;
|
|
553
|
+
@synchronized (self.standaloneWaiters) {
|
|
554
|
+
NSMutableArray<FBPendingRequest *> *waiters = self.standaloneWaiters[key];
|
|
555
|
+
if (nil != waiters) {
|
|
556
|
+
[waiters addObject:waiter];
|
|
557
|
+
isInFlight = YES;
|
|
558
|
+
} else {
|
|
559
|
+
self.standaloneWaiters[key] = [NSMutableArray array];
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (isInFlight) {
|
|
563
|
+
// An identical request is already executing; it will deliver this connection's response too.
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
dispatch_queue_t queue = dispatch_queue_create(key.UTF8String, DISPATCH_QUEUE_SERIAL);
|
|
568
|
+
__weak typeof(self) weakSelf = self;
|
|
569
|
+
dispatch_async(queue, ^{
|
|
570
|
+
route.block(request, response);
|
|
571
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
572
|
+
if (nil == strongSelf) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
NSArray<FBPendingRequest *> *joinedWaiters;
|
|
576
|
+
@synchronized (strongSelf.standaloneWaiters) {
|
|
577
|
+
joinedWaiters = [strongSelf.standaloneWaiters[key] copy];
|
|
578
|
+
[strongSelf.standaloneWaiters removeObjectForKey:key];
|
|
579
|
+
}
|
|
580
|
+
for (FBPendingRequest *joinedWaiter in [@[waiter] arrayByAddingObjectsFromArray:joinedWaiters]) {
|
|
581
|
+
BOOL shouldRespond = (nil == sessionID) || [strongSelf untrackPendingRequest:joinedWaiter forSessionID:sessionID];
|
|
582
|
+
if (shouldRespond) {
|
|
583
|
+
[strongSelf writeResponse:response toClient:joinedWaiter.client];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
412
589
|
- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client
|
|
413
590
|
{
|
|
414
591
|
[self writeResponse:response toClient:client thenCloseConnection:NO];
|
|
@@ -439,7 +616,15 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
439
616
|
[weakSelf closeClient:client];
|
|
440
617
|
}];
|
|
441
618
|
} else {
|
|
619
|
+
// Sent before unblocking the next pipelined request, so responses can't reach the wire out of order.
|
|
442
620
|
[self.socket writeData:payload toClient:client];
|
|
621
|
+
@synchronized (self.connectionBuffers) {
|
|
622
|
+
[self.connectionsAwaitingResponse removeObject:client];
|
|
623
|
+
}
|
|
624
|
+
__weak typeof(self) weakSelf = self;
|
|
625
|
+
dispatch_async(self.bufferProcessingQueue, ^{
|
|
626
|
+
[weakSelf processBufferForClient:client];
|
|
627
|
+
});
|
|
443
628
|
}
|
|
444
629
|
}
|
|
445
630
|
|
|
@@ -447,6 +632,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
447
632
|
{
|
|
448
633
|
@synchronized (self.connectionBuffers) {
|
|
449
634
|
[self.connectionBuffers removeObjectForKey:client];
|
|
635
|
+
[self.connectionsAwaitingResponse removeObject:client];
|
|
450
636
|
}
|
|
451
637
|
nw_connection_cancel(client);
|
|
452
638
|
}
|
|
@@ -27,6 +27,9 @@ typedef __nonnull id<FBResponsePayload> (^FBRouteSyncHandler)(FBRouteRequest *re
|
|
|
27
27
|
/*! Route's path */
|
|
28
28
|
@property (nonatomic, copy, readonly) NSString *path;
|
|
29
29
|
|
|
30
|
+
/*! Whether this route bypasses the shared route queue - see -standalone */
|
|
31
|
+
@property (nonatomic, assign, readonly) BOOL isStandalone;
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
34
|
Convenience constructor for GET route with given pathPattern
|
|
32
35
|
*/
|
|
@@ -67,6 +70,12 @@ typedef __nonnull id<FBResponsePayload> (^FBRouteSyncHandler)(FBRouteRequest *re
|
|
|
67
70
|
*/
|
|
68
71
|
- (instancetype)withoutSession;
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
Chain-able constructor for a route that bypasses the shared route queue - see FBHTTPServer.h's
|
|
75
|
+
-handleMethod:withPath:standalone:block: for what that changes about how/when the handler runs.
|
|
76
|
+
*/
|
|
77
|
+
- (instancetype)standalone;
|
|
78
|
+
|
|
70
79
|
/**
|
|
71
80
|
Dispatches response for request
|
|
72
81
|
*/
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
@interface FBRoute ()
|
|
20
20
|
@property (nonatomic, assign, readwrite) BOOL requiresSession;
|
|
21
|
+
@property (nonatomic, assign, readwrite) BOOL isStandalone;
|
|
21
22
|
@property (nonatomic, copy, readwrite) NSString *verb;
|
|
22
23
|
@property (nonatomic, copy, readwrite) NSString *path;
|
|
23
24
|
|
|
@@ -126,10 +127,17 @@ static NSString *const FBRouteSessionPrefix = @"/session/:sessionID";
|
|
|
126
127
|
return self;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
- (instancetype)standalone
|
|
131
|
+
{
|
|
132
|
+
self.isStandalone = YES;
|
|
133
|
+
return self;
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
- (instancetype)respondWithBlock:(FBRouteSyncHandler)handler
|
|
130
137
|
{
|
|
131
138
|
FBRoute_Sync *route = [FBRoute_Sync withVerb:self.verb path:self.path requiresSession:self.requiresSession];
|
|
132
139
|
route.handler = handler;
|
|
140
|
+
route.isStandalone = self.isStandalone;
|
|
133
141
|
return route;
|
|
134
142
|
}
|
|
135
143
|
|
|
@@ -138,6 +146,7 @@ static NSString *const FBRouteSessionPrefix = @"/session/:sessionID";
|
|
|
138
146
|
FBRoute_TargetAction *route = [FBRoute_TargetAction withVerb:self.verb path:self.path requiresSession:self.requiresSession];
|
|
139
147
|
route.target = target;
|
|
140
148
|
route.action = action;
|
|
149
|
+
route.isStandalone = self.isStandalone;
|
|
141
150
|
return route;
|
|
142
151
|
}
|
|
143
152
|
|
|
@@ -16,6 +16,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
16
16
|
/** Bundle identifier of Mobile Safari browser */
|
|
17
17
|
extern NSString* const FB_SAFARI_BUNDLE_ID;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
Posted (synchronously, on whatever thread calls -kill) once a session has been torn down. The
|
|
21
|
+
notification's object is the FBSession instance that was killed - see -identifier.
|
|
22
|
+
*/
|
|
23
|
+
extern NSString* const FBSessionWasKilledNotification;
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
Class that represents testing session
|
|
21
27
|
*/
|
|
@@ -44,6 +50,13 @@ extern NSString* const FB_SAFARI_BUNDLE_ID;
|
|
|
44
50
|
|
|
45
51
|
+ (nullable instancetype)activeSession;
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
Kills the active session, if any, and blocks until its teardown - including one already started
|
|
55
|
+
by a concurrent caller - is fully finished. Call this before preparing/launching a replacement
|
|
56
|
+
application, so it can't race a still-in-progress termination of the outgoing one.
|
|
57
|
+
*/
|
|
58
|
+
+ (void)killActiveSessionAndWaitForTeardown;
|
|
59
|
+
|
|
47
60
|
/**
|
|
48
61
|
Fetches session for given identifier.
|
|
49
62
|
If identifier doesn't match activeSession identifier, will return nil.
|