appium-webdriveragent 16.7.1 → 16.7.2
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 +6 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.h +3 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +136 -51
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +25 -11
- package/WebDriverAgentLib/Routing/FBWebServer.m +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [16.7.2](https://github.com/appium/WebDriverAgent/compare/v16.7.1...v16.7.2) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* harden FBHTTPServer/FBTCPSocket against races and protocol gaps ([#1224](https://github.com/appium/WebDriverAgent/issues/1224)) ([cf4bb2b](https://github.com/appium/WebDriverAgent/commit/cf4bb2b57b4d3a55ee4bbe8f860b00b0e7f5a326))
|
|
6
|
+
|
|
1
7
|
## [16.7.1](https://github.com/appium/WebDriverAgent/compare/v16.7.0...v16.7.1) (2026-08-23)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.7.
|
|
18
|
+
<string>16.7.2</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.7.
|
|
22
|
+
<string>16.7.2</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// A minimal HTTP/1.1 server on top of FBTCPSocket (Network.framework-backed on every platform,
|
|
10
10
|
// since watchOS forbids BSD sockets outright - see FBTCPSocket.h/.m).
|
|
11
11
|
//
|
|
12
|
-
// No
|
|
13
|
-
//
|
|
12
|
+
// No range requests or request pipelining - just request line + headers + Content-Length body,
|
|
13
|
+
// and ":param" path matching. Any Transfer-Encoding is rejected outright (501) rather than
|
|
14
|
+
// silently mishandled, since no decoder is implemented.
|
|
14
15
|
|
|
15
16
|
@import Foundation;
|
|
16
17
|
|
|
@@ -41,6 +41,20 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
41
41
|
@end
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
// Cached result of parsing a connection's request line + headers, kept around while its body is
|
|
45
|
+
// still streaming in so a slow body doesn't cause the header block to be re-found and re-parsed
|
|
46
|
+
// on every single incoming TCP segment.
|
|
47
|
+
@interface FBPendingHTTPRequestHeader : NSObject
|
|
48
|
+
@property (nonatomic, copy) NSString *method;
|
|
49
|
+
@property (nonatomic, copy) NSString *pathAndQuery;
|
|
50
|
+
@property (nonatomic) NSUInteger bodyStart;
|
|
51
|
+
@property (nonatomic) NSUInteger contentLength;
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation FBPendingHTTPRequestHeader
|
|
55
|
+
@end
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
@interface FBHTTPServer () <FBTCPSocketDelegate>
|
|
45
59
|
|
|
46
60
|
@property (nonatomic, nullable, strong) FBTCPSocket *socket;
|
|
@@ -50,6 +64,9 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
50
64
|
@property (nonatomic, copy, nullable) NSString *interface;
|
|
51
65
|
// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead.
|
|
52
66
|
@property (nonatomic, strong) NSMapTable<id, NSMutableData *> *connectionBuffers;
|
|
67
|
+
// Per-client cache of the already-parsed request line + headers while its body is still
|
|
68
|
+
// arriving; nil while a client's next unread bytes start with an unparsed header block.
|
|
69
|
+
@property (nonatomic, strong) NSMapTable<id, FBPendingHTTPRequestHeader *> *pendingRequestHeaders;
|
|
53
70
|
|
|
54
71
|
@end
|
|
55
72
|
|
|
@@ -62,6 +79,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
62
79
|
_defaultHeaders = [NSMutableDictionary dictionary];
|
|
63
80
|
_connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
64
81
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
82
|
+
_pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
83
|
+
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
65
84
|
}
|
|
66
85
|
return self;
|
|
67
86
|
}
|
|
@@ -103,6 +122,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
103
122
|
error:nil];
|
|
104
123
|
NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath];
|
|
105
124
|
__block NSInteger diff = 0;
|
|
125
|
+
__block NSUInteger wildcardIndex = 0;
|
|
106
126
|
[paramRegex enumerateMatchesInString:escapedPath
|
|
107
127
|
options:(NSMatchingOptions)0
|
|
108
128
|
range:NSMakeRange(0, escapedPath.length)
|
|
@@ -111,7 +131,11 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
111
131
|
NSString *capturedString = [escapedPath substringWithRange:result.range];
|
|
112
132
|
NSString *replacementString;
|
|
113
133
|
if ([capturedString isEqualToString:@"*"]) {
|
|
114
|
-
|
|
134
|
+
// Only the first wildcard keeps the plain "wildcards" name - later ones get an index
|
|
135
|
+
// suffix so multiple "*" segments in one path don't overwrite each other's capture.
|
|
136
|
+
NSString *wildcardKey = 0 == wildcardIndex ? @"wildcards" : [NSString stringWithFormat:@"wildcards%lu", (unsigned long)wildcardIndex];
|
|
137
|
+
wildcardIndex++;
|
|
138
|
+
[keys addObject:wildcardKey];
|
|
115
139
|
replacementString = @"(.*?)";
|
|
116
140
|
} else {
|
|
117
141
|
NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]];
|
|
@@ -166,6 +190,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
166
190
|
self.socket = nil;
|
|
167
191
|
@synchronized (self.connectionBuffers) {
|
|
168
192
|
[self.connectionBuffers removeAllObjects];
|
|
193
|
+
[self.pendingRequestHeaders removeAllObjects];
|
|
169
194
|
}
|
|
170
195
|
_isRunning = NO;
|
|
171
196
|
}
|
|
@@ -183,6 +208,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
183
208
|
{
|
|
184
209
|
@synchronized (self.connectionBuffers) {
|
|
185
210
|
[self.connectionBuffers removeObjectForKey:client];
|
|
211
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
186
212
|
}
|
|
187
213
|
}
|
|
188
214
|
|
|
@@ -205,72 +231,130 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
205
231
|
{
|
|
206
232
|
while (YES) {
|
|
207
233
|
NSMutableData *buffer;
|
|
234
|
+
FBPendingHTTPRequestHeader *pending;
|
|
208
235
|
@synchronized (self.connectionBuffers) {
|
|
209
236
|
buffer = [self.connectionBuffers objectForKey:client];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
if (nil == buffer) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
pending = [self.pendingRequestHeaders objectForKey:client];
|
|
213
241
|
}
|
|
214
242
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
if (nil == pending) {
|
|
244
|
+
NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
|
|
245
|
+
if (NSNotFound == headerEndRange.location) {
|
|
246
|
+
// Wait for the rest of the header block to arrive.
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
219
249
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
250
|
+
NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)];
|
|
251
|
+
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
|
252
|
+
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
|
253
|
+
if (lines.count < 1) {
|
|
254
|
+
[self respondBadRequestToClient:client];
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
227
257
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
NSString *method = requestLineParts[0].uppercaseString;
|
|
234
|
-
NSString *pathAndQuery = requestLineParts[1];
|
|
235
|
-
|
|
236
|
-
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
237
|
-
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
238
|
-
NSString *line = lines[i];
|
|
239
|
-
NSRange colonRange = [line rangeOfString:@":"];
|
|
240
|
-
if (NSNotFound == colonRange.location) {
|
|
241
|
-
continue;
|
|
258
|
+
NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
|
|
259
|
+
if (requestLineParts.count < 2) {
|
|
260
|
+
[self respondBadRequestToClient:client];
|
|
261
|
+
return;
|
|
242
262
|
}
|
|
243
|
-
NSString *name = [line substringToIndex:colonRange.location];
|
|
244
|
-
NSString *value = [[line substringFromIndex:colonRange.location + 1]
|
|
245
|
-
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
|
246
|
-
requestHeaders[name.lowercaseString] = value;
|
|
247
|
-
}
|
|
248
263
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
265
|
+
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
266
|
+
NSString *line = lines[i];
|
|
267
|
+
NSRange colonRange = [line rangeOfString:@":"];
|
|
268
|
+
if (NSNotFound == colonRange.location) {
|
|
269
|
+
continue;
|
|
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;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
NSString *transferEncoding = requestHeaders[@"transfer-encoding"];
|
|
278
|
+
if (transferEncoding.length > 0) {
|
|
279
|
+
// No transfer decoder is implemented at all, so any encoding (chunked or otherwise -
|
|
280
|
+
// including a value only introduced by a duplicate header overwriting "chunked" above)
|
|
281
|
+
// is rejected rather than risking the body being misread as empty and desyncing the rest
|
|
282
|
+
// of the connection's request stream.
|
|
283
|
+
RouteResponse *notImplemented = [RouteResponse new];
|
|
284
|
+
id<FBResponsePayload> notImplementedPayload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"Transfer-Encoding is not supported"
|
|
285
|
+
traceback:nil]);
|
|
286
|
+
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
287
|
+
[self failClient:client withResponse:notImplemented];
|
|
288
|
+
return;
|
|
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
|
+
}
|
|
258
311
|
}
|
|
259
|
-
|
|
260
|
-
NSUInteger totalRequestLength = bodyStart + contentLength;
|
|
312
|
+
|
|
313
|
+
NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength;
|
|
261
314
|
if (buffer.length < totalRequestLength) {
|
|
262
|
-
// Wait for the rest of the body to arrive
|
|
315
|
+
// Wait for the rest of the body to arrive - the parsed header stays cached above, so this
|
|
316
|
+
// doesn't re-scan/re-parse the header block on every subsequently arriving chunk.
|
|
263
317
|
return;
|
|
264
318
|
}
|
|
265
319
|
|
|
266
|
-
NSData *body = contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(bodyStart, contentLength)] : [NSData data];
|
|
320
|
+
NSData *body = pending.contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(pending.bodyStart, pending.contentLength)] : [NSData data];
|
|
267
321
|
|
|
268
322
|
@synchronized (self.connectionBuffers) {
|
|
269
323
|
[buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0];
|
|
324
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
270
325
|
}
|
|
271
326
|
|
|
272
|
-
[self dispatchMethod:method pathAndQuery:pathAndQuery body:body client:client];
|
|
327
|
+
[self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Removes the client's buffered state and responds with a closing error response. Removing the
|
|
332
|
+
// buffer synchronously ensures any request bytes still streaming in for this connection are
|
|
333
|
+
// dropped rather than being re-parsed and re-triggering this same response.
|
|
334
|
+
- (void)failClient:(nw_connection_t)client withResponse:(RouteResponse *)response
|
|
335
|
+
{
|
|
336
|
+
@synchronized (self.connectionBuffers) {
|
|
337
|
+
[self.connectionBuffers removeObjectForKey:client];
|
|
338
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
273
339
|
}
|
|
340
|
+
[self applyDefaultHeadersToResponse:response];
|
|
341
|
+
[self writeResponse:response toClient:client thenCloseConnection:YES];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
- (void)respondBadRequestToClient:(nw_connection_t)client
|
|
345
|
+
{
|
|
346
|
+
RouteResponse *badRequest = [RouteResponse new];
|
|
347
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:@"The request could not be parsed as valid HTTP"
|
|
348
|
+
traceback:nil]);
|
|
349
|
+
[payload dispatchWithResponse:badRequest];
|
|
350
|
+
[self failClient:client withResponse:badRequest];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
- (void)applyDefaultHeadersToResponse:(RouteResponse *)response
|
|
354
|
+
{
|
|
355
|
+
[self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) {
|
|
356
|
+
[response setHeader:field value:value];
|
|
357
|
+
}];
|
|
274
358
|
}
|
|
275
359
|
|
|
276
360
|
- (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client
|
|
@@ -302,9 +386,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
302
386
|
NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"];
|
|
303
387
|
RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body];
|
|
304
388
|
RouteResponse *response = [RouteResponse new];
|
|
305
|
-
[self
|
|
306
|
-
[response setHeader:field value:value];
|
|
307
|
-
}];
|
|
389
|
+
[self applyDefaultHeadersToResponse:response];
|
|
308
390
|
|
|
309
391
|
void (^invoke)(void) = ^{
|
|
310
392
|
route.block(request, response);
|
|
@@ -323,6 +405,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
323
405
|
FBCommandStatus *status = [FBCommandStatus unknownCommandErrorWithMessage:nil
|
|
324
406
|
traceback:nil];
|
|
325
407
|
[FBResponseWithStatus(status) dispatchWithResponse:notFound];
|
|
408
|
+
[self applyDefaultHeadersToResponse:notFound];
|
|
326
409
|
[self writeResponse:notFound toClient:client];
|
|
327
410
|
}
|
|
328
411
|
|
|
@@ -383,6 +466,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
383
466
|
return @"Request Timeout";
|
|
384
467
|
} else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) {
|
|
385
468
|
return @"Request Entity Too Large";
|
|
469
|
+
} else if (kHTTPStatusCodeNotImplemented == statusCode) {
|
|
470
|
+
return @"Not Implemented";
|
|
386
471
|
} else if (kHTTPStatusCodeInternalServerError == statusCode) {
|
|
387
472
|
return @"Internal Server Error";
|
|
388
473
|
}
|
|
@@ -78,12 +78,18 @@
|
|
|
78
78
|
// NSLocalizedDescriptionKey must be a string, not the underlying NSError itself, or
|
|
79
79
|
// -[NSError localizedDescription] crashes trying to treat it as one.
|
|
80
80
|
NSError *underlyingError = nwError ? (NSError *)CFBridgingRelease(nw_error_copy_cf_error(nwError)) : nil;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if ([underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) {
|
|
82
|
+
// Surface POSIX errors (e.g. EADDRINUSE) directly, since callers like FBWebServer check
|
|
83
|
+
// for them by domain/code on the top-level error to decide whether to retry another port.
|
|
84
|
+
startupError = underlyingError;
|
|
85
|
+
} else {
|
|
86
|
+
NSMutableDictionary<NSString *, id> *userInfo = [NSMutableDictionary dictionary];
|
|
87
|
+
userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription ?: @"The TCP listener failed to start";
|
|
88
|
+
if (underlyingError) {
|
|
89
|
+
userInfo[NSUnderlyingErrorKey] = underlyingError;
|
|
90
|
+
}
|
|
91
|
+
startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo];
|
|
85
92
|
}
|
|
86
|
-
startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo];
|
|
87
93
|
dispatch_semaphore_signal(startupSemaphore);
|
|
88
94
|
}
|
|
89
95
|
});
|
|
@@ -98,6 +104,10 @@
|
|
|
98
104
|
if (error) {
|
|
99
105
|
*error = startupError;
|
|
100
106
|
}
|
|
107
|
+
// Cancel rather than just dropping our reference - otherwise a late ready/failed callback
|
|
108
|
+
// can still fire and the port stays bound at the OS level even though the caller was told
|
|
109
|
+
// startup failed.
|
|
110
|
+
nw_listener_cancel(listener);
|
|
101
111
|
self.listener = nil;
|
|
102
112
|
return NO;
|
|
103
113
|
}
|
|
@@ -144,11 +154,9 @@
|
|
|
144
154
|
}
|
|
145
155
|
if (nil != content) {
|
|
146
156
|
dispatch_data_t nonnullContent = (dispatch_data_t _Nonnull)content;
|
|
147
|
-
|
|
157
|
+
NSMutableData *data = [NSMutableData data];
|
|
148
158
|
dispatch_data_apply(nonnullContent, ^bool(dispatch_data_t _Nonnull region, size_t offset, const void * _Nonnull buffer, size_t size) {
|
|
149
|
-
|
|
150
|
-
[accumulated appendBytes:buffer length:size];
|
|
151
|
-
data = accumulated.copy;
|
|
159
|
+
[data appendBytes:buffer length:size];
|
|
152
160
|
return true;
|
|
153
161
|
});
|
|
154
162
|
if (data.length > 0) {
|
|
@@ -198,13 +206,19 @@
|
|
|
198
206
|
|
|
199
207
|
- (void)stop
|
|
200
208
|
{
|
|
209
|
+
NSArray<nw_connection_t> *clients;
|
|
201
210
|
@synchronized (self.connectedClients) {
|
|
202
|
-
|
|
211
|
+
clients = self.connectedClients.copy;
|
|
203
212
|
[self.connectedClients removeAllObjects];
|
|
213
|
+
}
|
|
214
|
+
// Cancel on socketQueue, the same queue every connection's send/receive is bound to (see
|
|
215
|
+
// -acceptConnection:), so a write already issued just before -stop (e.g. a shutdown route's
|
|
216
|
+
// response) is processed before the cancellation rather than racing it.
|
|
217
|
+
dispatch_async(self.socketQueue, ^{
|
|
204
218
|
for (nw_connection_t client in clients) {
|
|
205
219
|
nw_connection_cancel(client);
|
|
206
220
|
}
|
|
207
|
-
}
|
|
221
|
+
});
|
|
208
222
|
|
|
209
223
|
self.delegate = nil;
|
|
210
224
|
nw_listener_t listener = self.listener;
|
|
@@ -261,7 +261,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
[response respondWithString:@"Shutting down"];
|
|
264
|
-
|
|
264
|
+
// Deferred so the "Shutting down" response is written to the client before
|
|
265
|
+
// webServerDidRequestShutdown: tears down the server's socket out from under it.
|
|
266
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
267
|
+
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
|
|
268
|
+
});
|
|
265
269
|
}];
|
|
266
270
|
|
|
267
271
|
[self registerRouteHandlers:@[FBUnknownCommands.class]];
|