appium-webdriveragent 16.7.1 → 16.7.3
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/Commands/FBScreenshotCommands.m +2 -2
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +3 -5
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.h +28 -3
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +308 -37
- 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 +27 -12
- package/WebDriverAgentLib/Routing/FBWebServer.m +31 -2
- package/WebDriverAgentLib/Routing/RouteResponse.m +1 -1
- package/WebDriverAgentLib/Utilities/FBConfiguration.m +7 -10
- package/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +10 -7
- package/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +36 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [16.7.3](https://github.com/appium/WebDriverAgent/compare/v16.7.2...v16.7.3) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* let /status, /screenshot, and DELETE /session API methods to bypass the dispatch queue ([#1222](https://github.com/appium/WebDriverAgent/issues/1222)) ([f99b011](https://github.com/appium/WebDriverAgent/commit/f99b0111bba6f5cceabbddf1f9f0144d69d8f168))
|
|
6
|
+
|
|
7
|
+
## [16.7.2](https://github.com/appium/WebDriverAgent/compare/v16.7.1...v16.7.2) (2026-08-24)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* harden FBHTTPServer/FBTCPSocket against races and protocol gaps ([#1224](https://github.com/appium/WebDriverAgent/issues/1224)) ([cf4bb2b](https://github.com/appium/WebDriverAgent/commit/cf4bb2b57b4d3a55ee4bbe8f860b00b0e7f5a326))
|
|
12
|
+
|
|
1
13
|
## [16.7.1](https://github.com/appium/WebDriverAgent/compare/v16.7.0...v16.7.1) (2026-08-23)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -42,15 +42,5 @@ Then, you find `WebDriverAgentRunner-Runner-sim-<version>.zip` for iOS and `Web
|
|
|
42
42
|
|
|
43
43
|
[`WebDriverAgent` is BSD-licensed](LICENSE).
|
|
44
44
|
|
|
45
|
-
## Third Party Sources
|
|
46
|
-
|
|
47
|
-
WebDriverAgent depends on the following third-party frameworks:
|
|
48
|
-
- [CocoaHTTPServer](https://github.com/robbiehanson/CocoaHTTPServer)
|
|
49
|
-
- [RoutingHTTPServer](https://github.com/mattstevens/RoutingHTTPServer)
|
|
50
|
-
|
|
51
|
-
These projects haven't been maintained in a while. That's why the source code of these
|
|
52
|
-
projects has been integrated directly in the WebDriverAgent source tree.
|
|
53
|
-
|
|
54
|
-
You can find the source files and their licenses in the `WebDriverAgentLib/Vendor` directory.
|
|
55
45
|
|
|
56
46
|
Have fun!
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
{
|
|
19
19
|
return
|
|
20
20
|
@[
|
|
21
|
-
[[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
|
22
|
-
[[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
|
21
|
+
[[FBRoute GET:@"/screenshot"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
|
22
|
+
[[FBRoute GET:@"/screenshot"].standalone respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
|
23
23
|
];
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
[[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)],
|
|
48
48
|
[[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)],
|
|
49
49
|
[[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)],
|
|
50
|
-
[[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)],
|
|
51
|
-
[[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)],
|
|
50
|
+
[[FBRoute DELETE:@""].standalone respondWithTarget:self action:@selector(handleDeleteSession:)],
|
|
51
|
+
[[FBRoute GET:@"/status"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetStatus:)],
|
|
52
52
|
|
|
53
53
|
// Health check might modify simulator state so it should only be called in-between testing sessions
|
|
54
54
|
[[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)],
|
|
@@ -89,9 +89,7 @@
|
|
|
89
89
|
|
|
90
90
|
+ (id<FBResponsePayload>)handleCreateSession:(FBRouteRequest *)request
|
|
91
91
|
{
|
|
92
|
-
|
|
93
|
-
[FBSession.activeSession kill];
|
|
94
|
-
}
|
|
92
|
+
[FBSession killActiveSessionAndWaitForTeardown];
|
|
95
93
|
|
|
96
94
|
NSDictionary<NSString *, id> *capabilities;
|
|
97
95
|
id<FBResponsePayload> errorResponse = [self capabilitiesFromCreateSessionRequest:request
|
|
@@ -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.3</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.7.
|
|
22
|
+
<string>16.7.3</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
|
|
|
@@ -46,17 +47,41 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
Registers a route handler for the given HTTP method and path pattern (":param" segments are
|
|
49
|
-
captured into the request's `params`).
|
|
50
|
+
captured into the request's `params`). Equivalent to -handleMethod:withPath:standalone:block:
|
|
51
|
+
with standalone:NO.
|
|
50
52
|
*/
|
|
51
53
|
- (void)handleMethod:(NSString *)method
|
|
52
54
|
withPath:(NSString *)path
|
|
53
55
|
block:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
Registers a route handler that, when `standalone` is YES, bypasses -routeQueue entirely so a
|
|
59
|
+
handler stuck on that queue can never block it. Concurrent requests to the same method+path are
|
|
60
|
+
coalesced into a single in-flight execution, whose response is delivered to all of them; anything
|
|
61
|
+
else runs on its own queue, so distinct standalone endpoints always execute in parallel with each
|
|
62
|
+
other and with whatever is stuck on -routeQueue.
|
|
63
|
+
*/
|
|
64
|
+
- (void)handleMethod:(NSString *)method
|
|
65
|
+
withPath:(NSString *)path
|
|
66
|
+
standalone:(BOOL)standalone
|
|
67
|
+
block:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
70
|
Convenience for -handleMethod:@"GET" withPath:path block:block.
|
|
57
71
|
*/
|
|
58
72
|
- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block;
|
|
59
73
|
|
|
74
|
+
/**
|
|
75
|
+
Immediately sends `response` to every non-standalone request currently pending for the given
|
|
76
|
+
"sessionID" path param - whether still queued on -routeQueue or already executing - instead of
|
|
77
|
+
leaving their HTTP clients waiting on a session that no longer exists. A request that has already
|
|
78
|
+
started executing keeps running to completion in the background regardless (GCD gives no way to
|
|
79
|
+
abort a block once it starts), but its eventual result is discarded rather than ever reaching a
|
|
80
|
+
client. `response` is written as-is to every pending client, so the caller is expected to supply
|
|
81
|
+
a fully-populated, protocol-correct error response (e.g. a W3C-shaped JSON body).
|
|
82
|
+
*/
|
|
83
|
+
- (void)abandonPendingRequestsForSessionID:(NSString *)sessionID withResponse:(RouteResponse *)response;
|
|
84
|
+
|
|
60
85
|
/**
|
|
61
86
|
Starts listening on `port`.
|
|
62
87
|
*/
|
|
@@ -35,12 +35,46 @@ 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
|
|
41
42
|
@end
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
// Cached result of parsing a connection's request line + headers, kept around while its body is
|
|
46
|
+
// still streaming in so a slow body doesn't cause the header block to be re-found and re-parsed
|
|
47
|
+
// on every single incoming TCP segment.
|
|
48
|
+
@interface FBPendingHTTPRequestHeader : NSObject
|
|
49
|
+
@property (nonatomic, copy) NSString *method;
|
|
50
|
+
@property (nonatomic, copy) NSString *pathAndQuery;
|
|
51
|
+
@property (nonatomic) NSUInteger bodyStart;
|
|
52
|
+
@property (nonatomic) NSUInteger contentLength;
|
|
53
|
+
@end
|
|
54
|
+
|
|
55
|
+
@implementation FBPendingHTTPRequestHeader
|
|
56
|
+
@end
|
|
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
|
+
|
|
44
78
|
@interface FBHTTPServer () <FBTCPSocketDelegate>
|
|
45
79
|
|
|
46
80
|
@property (nonatomic, nullable, strong) FBTCPSocket *socket;
|
|
@@ -50,6 +84,23 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
50
84
|
@property (nonatomic, copy, nullable) NSString *interface;
|
|
51
85
|
// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead.
|
|
52
86
|
@property (nonatomic, strong) NSMapTable<id, NSMutableData *> *connectionBuffers;
|
|
87
|
+
// Per-client cache of the already-parsed request line + headers while its body is still
|
|
88
|
+
// arriving; nil while a client's next unread bytes start with an unparsed header block.
|
|
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;
|
|
53
104
|
|
|
54
105
|
@end
|
|
55
106
|
|
|
@@ -62,6 +113,12 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
62
113
|
_defaultHeaders = [NSMutableDictionary dictionary];
|
|
63
114
|
_connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
64
115
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
116
|
+
_pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
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];
|
|
65
122
|
}
|
|
66
123
|
return self;
|
|
67
124
|
}
|
|
@@ -88,8 +145,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
88
145
|
FBHTTPRoute *route = [FBHTTPRoute new];
|
|
89
146
|
NSMutableArray<NSString *> *keys = [NSMutableArray array];
|
|
90
147
|
|
|
91
|
-
// Escape regex-significant characters before substituting :param placeholders
|
|
92
|
-
// RoutingHTTPServer.m used to.
|
|
148
|
+
// Escape regex-significant characters before substituting :param placeholders.
|
|
93
149
|
NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]"
|
|
94
150
|
options:(NSRegularExpressionOptions)0
|
|
95
151
|
error:nil];
|
|
@@ -103,6 +159,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
103
159
|
error:nil];
|
|
104
160
|
NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath];
|
|
105
161
|
__block NSInteger diff = 0;
|
|
162
|
+
__block NSUInteger wildcardIndex = 0;
|
|
106
163
|
[paramRegex enumerateMatchesInString:escapedPath
|
|
107
164
|
options:(NSMatchingOptions)0
|
|
108
165
|
range:NSMakeRange(0, escapedPath.length)
|
|
@@ -111,7 +168,11 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
111
168
|
NSString *capturedString = [escapedPath substringWithRange:result.range];
|
|
112
169
|
NSString *replacementString;
|
|
113
170
|
if ([capturedString isEqualToString:@"*"]) {
|
|
114
|
-
|
|
171
|
+
// Only the first wildcard keeps the plain "wildcards" name - later ones get an index
|
|
172
|
+
// suffix so multiple "*" segments in one path don't overwrite each other's capture.
|
|
173
|
+
NSString *wildcardKey = 0 == wildcardIndex ? @"wildcards" : [NSString stringWithFormat:@"wildcards%lu", (unsigned long)wildcardIndex];
|
|
174
|
+
wildcardIndex++;
|
|
175
|
+
[keys addObject:wildcardKey];
|
|
115
176
|
replacementString = @"(.*?)";
|
|
116
177
|
} else {
|
|
117
178
|
NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]];
|
|
@@ -133,10 +194,19 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
133
194
|
- (void)handleMethod:(NSString *)method
|
|
134
195
|
withPath:(NSString *)path
|
|
135
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
|
|
136
205
|
{
|
|
137
206
|
FBHTTPRoute *route = [self compiledRouteWithPath:path];
|
|
138
207
|
route.verb = method.uppercaseString;
|
|
139
208
|
route.block = block;
|
|
209
|
+
route.isStandalone = standalone;
|
|
140
210
|
[self.routes addObject:route];
|
|
141
211
|
}
|
|
142
212
|
|
|
@@ -166,6 +236,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
166
236
|
self.socket = nil;
|
|
167
237
|
@synchronized (self.connectionBuffers) {
|
|
168
238
|
[self.connectionBuffers removeAllObjects];
|
|
239
|
+
[self.pendingRequestHeaders removeAllObjects];
|
|
240
|
+
[self.connectionsAwaitingResponse removeAllObjects];
|
|
169
241
|
}
|
|
170
242
|
_isRunning = NO;
|
|
171
243
|
}
|
|
@@ -183,37 +255,56 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
183
255
|
{
|
|
184
256
|
@synchronized (self.connectionBuffers) {
|
|
185
257
|
[self.connectionBuffers removeObjectForKey:client];
|
|
258
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
259
|
+
[self.connectionsAwaitingResponse removeObject:client];
|
|
186
260
|
}
|
|
187
261
|
}
|
|
188
262
|
|
|
189
263
|
- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data
|
|
190
264
|
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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) {
|
|
195
272
|
return;
|
|
196
273
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
283
|
}
|
|
201
284
|
|
|
202
285
|
#pragma mark - HTTP parsing
|
|
203
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.
|
|
204
289
|
- (void)processBufferForClient:(nw_connection_t)client
|
|
205
290
|
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
291
|
+
NSMutableData *buffer;
|
|
292
|
+
FBPendingHTTPRequestHeader *pending;
|
|
293
|
+
@synchronized (self.connectionBuffers) {
|
|
294
|
+
if ([self.connectionsAwaitingResponse containsObject:client]) {
|
|
295
|
+
return;
|
|
210
296
|
}
|
|
297
|
+
buffer = [self.connectionBuffers objectForKey:client];
|
|
211
298
|
if (nil == buffer) {
|
|
212
299
|
return;
|
|
213
300
|
}
|
|
301
|
+
pending = [self.pendingRequestHeaders objectForKey:client];
|
|
302
|
+
}
|
|
214
303
|
|
|
304
|
+
if (nil == pending) {
|
|
215
305
|
NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)];
|
|
216
306
|
if (NSNotFound == headerEndRange.location) {
|
|
307
|
+
// Wait for the rest of the header block to arrive.
|
|
217
308
|
return;
|
|
218
309
|
}
|
|
219
310
|
|
|
@@ -221,17 +312,15 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
221
312
|
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
|
222
313
|
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
|
223
314
|
if (lines.count < 1) {
|
|
224
|
-
[self
|
|
315
|
+
[self respondBadRequestToClient:client];
|
|
225
316
|
return;
|
|
226
317
|
}
|
|
227
318
|
|
|
228
319
|
NSArray<NSString *> *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "];
|
|
229
320
|
if (requestLineParts.count < 2) {
|
|
230
|
-
[self
|
|
321
|
+
[self respondBadRequestToClient:client];
|
|
231
322
|
return;
|
|
232
323
|
}
|
|
233
|
-
NSString *method = requestLineParts[0].uppercaseString;
|
|
234
|
-
NSString *pathAndQuery = requestLineParts[1];
|
|
235
324
|
|
|
236
325
|
NSMutableDictionary<NSString *, NSString *> *requestHeaders = [NSMutableDictionary dictionary];
|
|
237
326
|
for (NSUInteger i = 1; i < lines.count; i++) {
|
|
@@ -246,31 +335,86 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
246
335
|
requestHeaders[name.lowercaseString] = value;
|
|
247
336
|
}
|
|
248
337
|
|
|
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"
|
|
346
|
+
traceback:nil]);
|
|
347
|
+
[notImplementedPayload dispatchWithResponse:notImplemented];
|
|
348
|
+
[self failClient:client withResponse:notImplemented];
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
249
352
|
NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue;
|
|
250
353
|
if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) {
|
|
251
|
-
//
|
|
252
|
-
// responding, since the rest of the oversized body is still incoming.
|
|
354
|
+
// Closes the connection after responding, since the rest of the oversized body is still incoming.
|
|
253
355
|
RouteResponse *tooLarge = [RouteResponse new];
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
[
|
|
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];
|
|
257
360
|
return;
|
|
258
361
|
}
|
|
259
|
-
NSUInteger bodyStart = headerEndRange.location + headerEndRange.length;
|
|
260
|
-
NSUInteger totalRequestLength = bodyStart + contentLength;
|
|
261
|
-
if (buffer.length < totalRequestLength) {
|
|
262
|
-
// Wait for the rest of the body to arrive.
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
NSData *body = contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(bodyStart, contentLength)] : [NSData data];
|
|
267
362
|
|
|
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;
|
|
268
368
|
@synchronized (self.connectionBuffers) {
|
|
269
|
-
[
|
|
369
|
+
[self.pendingRequestHeaders setObject:pending forKey:client];
|
|
270
370
|
}
|
|
371
|
+
}
|
|
271
372
|
|
|
272
|
-
|
|
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;
|
|
273
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];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Removes the client's buffered state and responds with a closing error response. Removing the
|
|
392
|
+
// buffer synchronously ensures any request bytes still streaming in for this connection are
|
|
393
|
+
// dropped rather than being re-parsed and re-triggering this same response.
|
|
394
|
+
- (void)failClient:(nw_connection_t)client withResponse:(RouteResponse *)response
|
|
395
|
+
{
|
|
396
|
+
@synchronized (self.connectionBuffers) {
|
|
397
|
+
[self.connectionBuffers removeObjectForKey:client];
|
|
398
|
+
[self.pendingRequestHeaders removeObjectForKey:client];
|
|
399
|
+
}
|
|
400
|
+
[self applyDefaultHeadersToResponse:response];
|
|
401
|
+
[self writeResponse:response toClient:client thenCloseConnection:YES];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
- (void)respondBadRequestToClient:(nw_connection_t)client
|
|
405
|
+
{
|
|
406
|
+
RouteResponse *badRequest = [RouteResponse new];
|
|
407
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request could not be parsed as valid HTTP"
|
|
408
|
+
traceback:nil]);
|
|
409
|
+
[payload dispatchWithResponse:badRequest];
|
|
410
|
+
[self failClient:client withResponse:badRequest];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
- (void)applyDefaultHeadersToResponse:(RouteResponse *)response
|
|
414
|
+
{
|
|
415
|
+
[self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) {
|
|
416
|
+
[response setHeader:field value:value];
|
|
417
|
+
}];
|
|
274
418
|
}
|
|
275
419
|
|
|
276
420
|
- (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client
|
|
@@ -302,13 +446,31 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
302
446
|
NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"];
|
|
303
447
|
RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body];
|
|
304
448
|
RouteResponse *response = [RouteResponse new];
|
|
305
|
-
[self
|
|
306
|
-
|
|
307
|
-
|
|
449
|
+
[self applyDefaultHeadersToResponse:response];
|
|
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
|
+
}
|
|
308
465
|
|
|
309
466
|
void (^invoke)(void) = ^{
|
|
310
467
|
route.block(request, response);
|
|
311
|
-
|
|
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
|
+
}
|
|
312
474
|
};
|
|
313
475
|
dispatch_queue_t routeQueue = self.routeQueue;
|
|
314
476
|
if (routeQueue) {
|
|
@@ -323,9 +485,107 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
323
485
|
FBCommandStatus *status = [FBCommandStatus unknownCommandErrorWithMessage:nil
|
|
324
486
|
traceback:nil];
|
|
325
487
|
[FBResponseWithStatus(status) dispatchWithResponse:notFound];
|
|
488
|
+
[self applyDefaultHeadersToResponse:notFound];
|
|
326
489
|
[self writeResponse:notFound toClient:client];
|
|
327
490
|
}
|
|
328
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
|
+
|
|
329
589
|
- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client
|
|
330
590
|
{
|
|
331
591
|
[self writeResponse:response toClient:client thenCloseConnection:NO];
|
|
@@ -356,7 +616,15 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
356
616
|
[weakSelf closeClient:client];
|
|
357
617
|
}];
|
|
358
618
|
} else {
|
|
619
|
+
// Sent before unblocking the next pipelined request, so responses can't reach the wire out of order.
|
|
359
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
|
+
});
|
|
360
628
|
}
|
|
361
629
|
}
|
|
362
630
|
|
|
@@ -364,6 +632,7 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
364
632
|
{
|
|
365
633
|
@synchronized (self.connectionBuffers) {
|
|
366
634
|
[self.connectionBuffers removeObjectForKey:client];
|
|
635
|
+
[self.connectionsAwaitingResponse removeObject:client];
|
|
367
636
|
}
|
|
368
637
|
nw_connection_cancel(client);
|
|
369
638
|
}
|
|
@@ -383,6 +652,8 @@ static NSData * _Nonnull FBUTF8Data(NSString *string)
|
|
|
383
652
|
return @"Request Timeout";
|
|
384
653
|
} else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) {
|
|
385
654
|
return @"Request Entity Too Large";
|
|
655
|
+
} else if (kHTTPStatusCodeNotImplemented == statusCode) {
|
|
656
|
+
return @"Not Implemented";
|
|
386
657
|
} else if (kHTTPStatusCodeInternalServerError == statusCode) {
|
|
387
658
|
return @"Internal Server Error";
|
|
388
659
|
}
|
|
@@ -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.
|
|
@@ -34,12 +34,23 @@ NSString *const FBDefaultApplicationAuto = @"auto";
|
|
|
34
34
|
|
|
35
35
|
NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
|
|
36
36
|
|
|
37
|
+
// FBXCAXClientProxy's shared accessibility channel can be stuck servicing another request.
|
|
38
|
+
static const NSTimeInterval FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC = 5.;
|
|
39
|
+
// -terminate hard-asserts off the main thread, which may itself be busy - see -fb_terminate...:.
|
|
40
|
+
static const NSTimeInterval FB_APP_TERMINATE_TIMEOUT_SEC = 5.;
|
|
41
|
+
// How long a -kill caller that lost the race below waits for the winner's teardown to finish.
|
|
42
|
+
static const NSTimeInterval FB_KILL_WAIT_TIMEOUT_SEC = 35.;
|
|
43
|
+
NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotification";
|
|
44
|
+
|
|
37
45
|
@interface FBSession ()
|
|
38
46
|
@property (nullable, nonatomic) XCUIApplication *testedApplication;
|
|
39
47
|
@property (nonatomic) BOOL isTestedApplicationExpectedToRun;
|
|
40
48
|
@property (nonatomic) BOOL shouldAppsWaitForQuiescence;
|
|
41
49
|
@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
|
|
42
50
|
@property (nonatomic, readwrite) NSMutableDictionary<NSNumber *, NSMutableDictionary<NSString *, NSNumber *> *> *elementsVisibilityCache;
|
|
51
|
+
|
|
52
|
+
- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout;
|
|
53
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout;
|
|
43
54
|
@end
|
|
44
55
|
|
|
45
56
|
@interface FBSession (FBAlertsMonitorDelegate)
|
|
@@ -90,17 +101,54 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
|
|
|
90
101
|
@implementation FBSession
|
|
91
102
|
|
|
92
103
|
static FBSession *_activeSession = nil;
|
|
104
|
+
// Class-level, not per-instance: a caller that finds _activeSession already nil (a concurrent
|
|
105
|
+
// -kill beat it there) still needs to know whether that -kill's teardown is done, since it cleared
|
|
106
|
+
// the pointer before running it. See +waitForActiveTeardownWithTimeout:.
|
|
107
|
+
static BOOL _isTeardownInProgress = NO;
|
|
108
|
+
|
|
109
|
+
+ (NSCondition *)teardownCondition
|
|
110
|
+
{
|
|
111
|
+
static NSCondition *condition;
|
|
112
|
+
static dispatch_once_t onceToken;
|
|
113
|
+
dispatch_once(&onceToken, ^{
|
|
114
|
+
condition = [NSCondition new];
|
|
115
|
+
});
|
|
116
|
+
return condition;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Waits (bounded) for any -kill teardown currently in progress to finish.
|
|
120
|
+
+ (void)waitForActiveTeardownWithTimeout:(NSTimeInterval)timeout
|
|
121
|
+
{
|
|
122
|
+
NSCondition *condition = self.teardownCondition;
|
|
123
|
+
[condition lock];
|
|
124
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
|
|
125
|
+
while (_isTeardownInProgress && [condition waitUntilDate:deadline]) {
|
|
126
|
+
}
|
|
127
|
+
[condition unlock];
|
|
128
|
+
}
|
|
93
129
|
|
|
94
130
|
+ (instancetype)activeSession
|
|
95
131
|
{
|
|
96
132
|
return _activeSession;
|
|
97
133
|
}
|
|
98
134
|
|
|
99
|
-
+ (void)
|
|
135
|
+
+ (void)killActiveSessionAndWaitForTeardown
|
|
100
136
|
{
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
FBSession *session = _activeSession;
|
|
138
|
+
if (nil != session) {
|
|
139
|
+
// Runs the real teardown synchronously if this call wins the race in -kill, or waits for
|
|
140
|
+
// whoever did to finish if it lost - either way, blocks until torn down.
|
|
141
|
+
[session kill];
|
|
142
|
+
} else {
|
|
143
|
+
// _activeSession is already nil, but a concurrent -kill (e.g. from DELETE /session) may still
|
|
144
|
+
// be mid-teardown - wait for it, so we don't launch a replacement app too early.
|
|
145
|
+
[self waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC];
|
|
103
146
|
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
+ (void)markSessionActive:(FBSession *)session
|
|
150
|
+
{
|
|
151
|
+
[self killActiveSessionAndWaitForTeardown];
|
|
104
152
|
_activeSession = session;
|
|
105
153
|
}
|
|
106
154
|
|
|
@@ -169,33 +217,58 @@ static FBSession *_activeSession = nil;
|
|
|
169
217
|
|
|
170
218
|
- (void)kill
|
|
171
219
|
{
|
|
172
|
-
|
|
220
|
+
// DELETE /session and session creation can now run concurrently, so a session already
|
|
221
|
+
// superseded by a newer one can still reach here via a stale reference. Check-and-clear must be
|
|
222
|
+
// atomic, else a belated -kill could null out the new session's pointer instead of its own.
|
|
223
|
+
BOOL wasActive;
|
|
224
|
+
@synchronized (self.class) {
|
|
225
|
+
wasActive = (self == _activeSession);
|
|
226
|
+
if (wasActive) {
|
|
227
|
+
_activeSession = nil;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!wasActive) {
|
|
231
|
+
// Someone else is already tearing this session down - wait for that to finish (bounded), so
|
|
232
|
+
// we don't act as if it's gone (e.g. launch a new app) while its -terminate is still in flight.
|
|
233
|
+
[self.class waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC];
|
|
173
234
|
return;
|
|
174
235
|
}
|
|
175
236
|
|
|
176
|
-
|
|
237
|
+
NSCondition *teardownCondition = self.class.teardownCondition;
|
|
238
|
+
[teardownCondition lock];
|
|
239
|
+
_isTeardownInProgress = YES;
|
|
240
|
+
[teardownCondition unlock];
|
|
241
|
+
|
|
242
|
+
@try {
|
|
243
|
+
// Posted before teardown so pending HTTP requests for this session can stop waiting sooner.
|
|
244
|
+
[NSNotificationCenter.defaultCenter postNotificationName:FBSessionWasKilledNotification object:self];
|
|
177
245
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
246
|
+
[self disableAlertsMonitor];
|
|
247
|
+
|
|
248
|
+
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
|
|
249
|
+
if (nil != activeScreenRecording) {
|
|
250
|
+
NSError *error;
|
|
251
|
+
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) {
|
|
252
|
+
[FBLogger logFmt:@"%@", error];
|
|
253
|
+
}
|
|
254
|
+
[FBScreenRecordingContainer.sharedInstance reset];
|
|
183
255
|
}
|
|
184
|
-
[FBScreenRecordingContainer.sharedInstance reset];
|
|
185
|
-
}
|
|
186
256
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
[
|
|
257
|
+
if (nil != self.testedApplication
|
|
258
|
+
&& FBConfiguration.sharedInstance.shouldTerminateApp
|
|
259
|
+
&& self.testedApplication.running
|
|
260
|
+
&& ![self fb_isTestedApplicationSameAsSystemAppWithTimeout:FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC]) {
|
|
261
|
+
// Blocks until the app is either actually terminated or durably given up on (never left
|
|
262
|
+
// pending) - see -fb_terminateTestedApplicationWithTimeout: - so it's safe to report this
|
|
263
|
+
// teardown as finished as soon as this returns.
|
|
264
|
+
[self fb_terminateTestedApplicationWithTimeout:FB_APP_TERMINATE_TIMEOUT_SEC];
|
|
195
265
|
}
|
|
266
|
+
} @finally {
|
|
267
|
+
[teardownCondition lock];
|
|
268
|
+
_isTeardownInProgress = NO;
|
|
269
|
+
[teardownCondition broadcast];
|
|
270
|
+
[teardownCondition unlock];
|
|
196
271
|
}
|
|
197
|
-
|
|
198
|
-
_activeSession = nil;
|
|
199
272
|
}
|
|
200
273
|
|
|
201
274
|
- (XCUIApplication *)activeApplication
|
|
@@ -290,4 +363,62 @@ static FBSession *_activeSession = nil;
|
|
|
290
363
|
: [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier];
|
|
291
364
|
}
|
|
292
365
|
|
|
366
|
+
// Has no async variant and can block on the shared accessibility channel. Run off-thread and give
|
|
367
|
+
// up after `timeout`, assuming the tested app IS the system app - safer, since it means skipping
|
|
368
|
+
// termination rather than risking terminating springboard.
|
|
369
|
+
- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout
|
|
370
|
+
{
|
|
371
|
+
__block XCUIApplication *systemApp = nil;
|
|
372
|
+
__block NSException *caughtException = nil;
|
|
373
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
374
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
375
|
+
// Undocumented private API; guard in case it hard-asserts off-main like -terminate does on
|
|
376
|
+
// some Xcode/iOS version - uncaught, that would crash the whole process.
|
|
377
|
+
@try {
|
|
378
|
+
systemApp = XCUIApplication.fb_systemApplication;
|
|
379
|
+
} @catch (NSException *e) {
|
|
380
|
+
caughtException = e;
|
|
381
|
+
}
|
|
382
|
+
dispatch_semaphore_signal(sem);
|
|
383
|
+
});
|
|
384
|
+
int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
|
|
385
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) || nil != caughtException) {
|
|
386
|
+
[FBLogger logFmt:@"Could not determine the system application within %@ seconds%@; assuming '%@' might be it and skipping its termination", @(timeout), nil == caughtException ? @"" : [NSString stringWithFormat:@" (%@)", caughtException.description], self.testedApplication.bundleID];
|
|
387
|
+
return YES;
|
|
388
|
+
}
|
|
389
|
+
return [self.testedApplication fb_isSameAppAs:systemApp];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// -terminate hard-asserts off-main, but -kill can now run on a background queue. Dispatching to
|
|
393
|
+
// main and waiting indefinitely could hang just as long as main is stuck, so give up after
|
|
394
|
+
// `timeout` - but a "given up on" call must never still terminate whatever's running by the time
|
|
395
|
+
// main gets to it (e.g. a replacement session's app), so cancellation and the actual terminate
|
|
396
|
+
// call share a lock: whichever gets there first - the dispatched block, or the timeout - wins.
|
|
397
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout
|
|
398
|
+
{
|
|
399
|
+
XCUIApplication *application = self.testedApplication;
|
|
400
|
+
NSObject *lock = [NSObject new];
|
|
401
|
+
__block BOOL isAllowedToTerminate = YES;
|
|
402
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
403
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
404
|
+
@synchronized (lock) {
|
|
405
|
+
if (isAllowedToTerminate) {
|
|
406
|
+
@try {
|
|
407
|
+
[application terminate];
|
|
408
|
+
} @catch (NSException *e) {
|
|
409
|
+
[FBLogger logFmt:@"%@", e.description];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
dispatch_semaphore_signal(sem);
|
|
414
|
+
});
|
|
415
|
+
int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
|
|
416
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
|
417
|
+
@synchronized (lock) {
|
|
418
|
+
isAllowedToTerminate = NO;
|
|
419
|
+
}
|
|
420
|
+
[FBLogger logFmt:@"Could not terminate '%@' within %@ seconds; giving up on it rather than risk terminating a possible replacement session's app later", application.bundleID, @(timeout)];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
293
424
|
@end
|
|
@@ -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
|
}
|
|
@@ -125,7 +135,8 @@
|
|
|
125
135
|
}
|
|
126
136
|
[strongSelf scheduleReceiveForConnection:connection];
|
|
127
137
|
} else if (nw_connection_state_failed == state || nw_connection_state_cancelled == state) {
|
|
128
|
-
|
|
138
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
139
|
+
[strongSelf handleDisconnectForConnection:connection];
|
|
129
140
|
}
|
|
130
141
|
});
|
|
131
142
|
nw_connection_start(connection);
|
|
@@ -144,11 +155,9 @@
|
|
|
144
155
|
}
|
|
145
156
|
if (nil != content) {
|
|
146
157
|
dispatch_data_t nonnullContent = (dispatch_data_t _Nonnull)content;
|
|
147
|
-
|
|
158
|
+
NSMutableData *data = [NSMutableData data];
|
|
148
159
|
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;
|
|
160
|
+
[data appendBytes:buffer length:size];
|
|
152
161
|
return true;
|
|
153
162
|
});
|
|
154
163
|
if (data.length > 0) {
|
|
@@ -198,13 +207,19 @@
|
|
|
198
207
|
|
|
199
208
|
- (void)stop
|
|
200
209
|
{
|
|
210
|
+
NSArray<nw_connection_t> *clients;
|
|
201
211
|
@synchronized (self.connectedClients) {
|
|
202
|
-
|
|
212
|
+
clients = self.connectedClients.copy;
|
|
203
213
|
[self.connectedClients removeAllObjects];
|
|
214
|
+
}
|
|
215
|
+
// Cancel on socketQueue, the same queue every connection's send/receive is bound to (see
|
|
216
|
+
// -acceptConnection:), so a write already issued just before -stop (e.g. a shutdown route's
|
|
217
|
+
// response) is processed before the cancellation rather than racing it.
|
|
218
|
+
dispatch_async(self.socketQueue, ^{
|
|
204
219
|
for (nw_connection_t client in clients) {
|
|
205
220
|
nw_connection_cancel(client);
|
|
206
221
|
}
|
|
207
|
-
}
|
|
222
|
+
});
|
|
208
223
|
|
|
209
224
|
self.delegate = nil;
|
|
210
225
|
nw_listener_t listener = self.listener;
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
#import "FBTCPSocket.h"
|
|
14
14
|
|
|
15
15
|
#import "FBCommandHandler.h"
|
|
16
|
+
#import "FBCommandStatus.h"
|
|
16
17
|
#import "FBErrorBuilder.h"
|
|
17
18
|
#import "FBExceptionHandler.h"
|
|
19
|
+
#import "FBResponsePayload.h"
|
|
18
20
|
#import "FBRouteRequest.h"
|
|
19
21
|
#import "FBRuntimeUtils.h"
|
|
20
22
|
#import "FBSession.h"
|
|
@@ -80,6 +82,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
80
82
|
[self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"];
|
|
81
83
|
[self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"];
|
|
82
84
|
|
|
85
|
+
[NSNotificationCenter.defaultCenter addObserver:self
|
|
86
|
+
selector:@selector(sessionWasKilled:)
|
|
87
|
+
name:FBSessionWasKilledNotification
|
|
88
|
+
object:nil];
|
|
89
|
+
|
|
83
90
|
[self registerRouteHandlers:[self.class collectCommandHandlerClasses]];
|
|
84
91
|
[self registerServerKeyRouteHandlers];
|
|
85
92
|
|
|
@@ -167,8 +174,26 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
- (void)sessionWasKilled:(NSNotification *)notification
|
|
178
|
+
{
|
|
179
|
+
FBSession *session = notification.object;
|
|
180
|
+
if (![session isKindOfClass:FBSession.class]) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Same "invalid session id" shape a still-queued request would eventually get anyway, once
|
|
184
|
+
// -routeQueue drains and FBRoute.decorateRequest: finds the session gone - just delivered now
|
|
185
|
+
// instead of after however long the request would otherwise have been stuck waiting.
|
|
186
|
+
NSString *message = [NSString stringWithFormat:@"Session %@ was deleted while this request was still pending", session.identifier];
|
|
187
|
+
id<FBResponsePayload> payload = FBResponseWithStatus([FBCommandStatus noSuchDriverErrorWithMessage:message
|
|
188
|
+
traceback:nil]);
|
|
189
|
+
RouteResponse *response = [RouteResponse new];
|
|
190
|
+
[payload dispatchWithResponse:response];
|
|
191
|
+
[self.server abandonPendingRequestsForSessionID:session.identifier withResponse:response];
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
- (void)stopServing
|
|
171
195
|
{
|
|
196
|
+
[NSNotificationCenter.defaultCenter removeObserver:self name:FBSessionWasKilledNotification object:nil];
|
|
172
197
|
[FBSession.activeSession kill];
|
|
173
198
|
[self stopScreenshotsBroadcaster];
|
|
174
199
|
if (self.server.isRunning) {
|
|
@@ -208,7 +233,7 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
208
233
|
for (Class<FBCommandHandler> commandHandler in commandHandlerClasses) {
|
|
209
234
|
NSArray *routes = [commandHandler routes];
|
|
210
235
|
for (FBRoute *route in routes) {
|
|
211
|
-
[self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) {
|
|
236
|
+
[self.server handleMethod:route.verb withPath:route.path standalone:route.isStandalone block:^(RouteRequest *request, RouteResponse *response) {
|
|
212
237
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
213
238
|
if (nil == strongSelf) {
|
|
214
239
|
return;
|
|
@@ -261,7 +286,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
261
286
|
return;
|
|
262
287
|
}
|
|
263
288
|
[response respondWithString:@"Shutting down"];
|
|
264
|
-
|
|
289
|
+
// Deferred so the "Shutting down" response is written to the client before
|
|
290
|
+
// webServerDidRequestShutdown: tears down the server's socket out from under it.
|
|
291
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
292
|
+
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
|
|
293
|
+
});
|
|
265
294
|
}];
|
|
266
295
|
|
|
267
296
|
[self registerRouteHandlers:@[FBUnknownCommands.class]];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#import "RouteResponse.h"
|
|
10
10
|
|
|
11
11
|
@interface RouteResponse ()
|
|
12
|
-
@property (nonatomic,
|
|
12
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *mutableHeaders;
|
|
13
13
|
@end
|
|
14
14
|
|
|
15
15
|
@implementation RouteResponse
|
|
@@ -124,17 +124,14 @@ static NSString *const axSettingsClassName = @"AXSettings";
|
|
|
124
124
|
{
|
|
125
125
|
// 'WebDriverAgent --port 8080' can be passed via the arguments to the process
|
|
126
126
|
NSRange rangeFromArguments = [self.class bindingPortRangeFromArguments];
|
|
127
|
-
if (rangeFromArguments.location
|
|
128
|
-
|
|
127
|
+
if (rangeFromArguments.location == NSNotFound) {
|
|
128
|
+
// Existence of USE_PORT in the environment implies the port range is managed by the launching process.
|
|
129
|
+
NSString *usePort = NSProcessInfo.processInfo.environment[@"USE_PORT"];
|
|
130
|
+
rangeFromArguments = usePort.length > 0
|
|
131
|
+
? NSMakeRange((NSUInteger)usePort.integerValue, 1)
|
|
132
|
+
: NSMakeRange(DefaultStartingPort, DefaultPortRange);
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
// Existence of USE_PORT in the environment implies the port range is managed by the launching process.
|
|
132
|
-
if (NSProcessInfo.processInfo.environment[@"USE_PORT"] &&
|
|
133
|
-
[NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) {
|
|
134
|
-
return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return NSMakeRange(DefaultStartingPort, DefaultPortRange);
|
|
134
|
+
return rangeFromArguments;
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
- (NSString *)bindingIPAddress
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
#import "XCUIDevice.h"
|
|
24
24
|
|
|
25
25
|
#define LAUNCH_APP_TIMEOUT_SEC 300
|
|
26
|
+
#define STOP_SCREEN_RECORDING_TIMEOUT_SEC 20
|
|
26
27
|
|
|
27
28
|
static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));
|
|
28
29
|
|
|
@@ -342,14 +343,16 @@ static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundl
|
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
__block NSError *innerError = nil;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
completion();
|
|
351
|
-
}];
|
|
346
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
347
|
+
[session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) {
|
|
348
|
+
innerError = invokeError;
|
|
349
|
+
dispatch_semaphore_signal(sem);
|
|
352
350
|
}];
|
|
351
|
+
int64_t timeoutNs = (int64_t)(STOP_SCREEN_RECORDING_TIMEOUT_SEC * NSEC_PER_SEC);
|
|
352
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) && nil == innerError) {
|
|
353
|
+
NSString *message = [NSString stringWithFormat:@"Did not receive a reply to stop screen recording within %d seconds", STOP_SCREEN_RECORDING_TIMEOUT_SEC];
|
|
354
|
+
innerError = [[[FBErrorBuilder builder] withDescription:message] build];
|
|
355
|
+
}
|
|
353
356
|
if (nil != innerError && error) {
|
|
354
357
|
*error = innerError;
|
|
355
358
|
}
|
|
@@ -77,32 +77,53 @@
|
|
|
77
77
|
|
|
78
78
|
@end
|
|
79
79
|
|
|
80
|
+
#define TESTMANAGERD_VERSION_TIMEOUT_SEC 20
|
|
81
|
+
|
|
80
82
|
NSInteger FBTestmanagerdVersion(void)
|
|
81
83
|
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
// Not dispatch_once: that would permanently cache the timeout fallback below if the first call's
|
|
85
|
+
// reply merely arrived late. -1 means "not yet determined"; a timeout isn't cached, so it retries.
|
|
86
|
+
static NSInteger cachedVersion = -1;
|
|
87
|
+
static dispatch_queue_t syncQueue;
|
|
88
|
+
static dispatch_once_t onceToken;
|
|
89
|
+
dispatch_once(&onceToken, ^{
|
|
90
|
+
syncQueue = dispatch_queue_create("com.facebook.wda.testmanagerdVersion", DISPATCH_QUEUE_SERIAL);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
__block NSInteger result;
|
|
94
|
+
dispatch_sync(syncQueue, ^{
|
|
95
|
+
if (cachedVersion >= 0) {
|
|
96
|
+
result = cachedVersion;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
id<XCTMessagingChannel_RunnerToDaemon> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
|
86
101
|
if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) {
|
|
87
102
|
id<FBXCTestManagerLegacyProtocolVersionExchanging> legacyProxy = (id<FBXCTestManagerLegacyProtocolVersionExchanging>)proxy;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
__block NSInteger receivedVersion = -1;
|
|
104
|
+
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
105
|
+
[legacyProxy _XCT_exchangeProtocolVersion:0 reply:^(unsigned long long code) {
|
|
106
|
+
receivedVersion = (NSInteger) code;
|
|
107
|
+
dispatch_semaphore_signal(sem);
|
|
93
108
|
}];
|
|
109
|
+
int64_t timeoutNs = (int64_t)(TESTMANAGERD_VERSION_TIMEOUT_SEC * NSEC_PER_SEC);
|
|
110
|
+
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
|
111
|
+
// Assume newest/full-featured on timeout, but don't cache it - retry on the next call.
|
|
112
|
+
[FBLogger logFmt:@"Did not receive a testmanagerd protocol version reply within %d seconds; assuming the newest/full-featured protocol", TESTMANAGERD_VERSION_TIMEOUT_SEC];
|
|
113
|
+
result = 0xFFFF;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
result = receivedVersion;
|
|
94
117
|
} else {
|
|
95
|
-
// Modern testmanagerd (Xcode 15+)
|
|
96
|
-
//
|
|
97
|
-
// integer equivalent to report here (this value is diagnostic-only, surfaced via the
|
|
98
|
-
// 'testmanagerdVersion' session capability), so keep reporting the existing "assume
|
|
99
|
-
// newest/full-featured" sentinel, while confirming capabilities did negotiate successfully.
|
|
118
|
+
// Modern testmanagerd (Xcode 15+) negotiates named XCTCapabilities instead of a scalar
|
|
119
|
+
// version; there's no direct integer equivalent, so just confirm capabilities negotiated.
|
|
100
120
|
XCTCapabilities *capabilities = [XCTRunnerDaemonSession sharedSession].remoteInterfaceCapabilities;
|
|
101
121
|
if (nil == capabilities) {
|
|
102
122
|
[FBLogger log:@"Could not retrieve testmanagerd capabilities"];
|
|
103
123
|
}
|
|
104
|
-
|
|
124
|
+
result = 0xFFFF;
|
|
105
125
|
}
|
|
126
|
+
cachedVersion = result;
|
|
106
127
|
});
|
|
107
|
-
return
|
|
128
|
+
return result;
|
|
108
129
|
}
|