appium-webdriveragent 16.11.0 → 16.11.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 +12 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +4 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBHTTPServer.m +25 -3
- package/WebDriverAgentLib/Utilities/FBMjpegServer.m +1 -1
- package/WebDriverAgentTests/UnitTests/FBHTTPServerSessionTests.m +133 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [16.11.2](https://github.com/appium/WebDriverAgent/compare/v16.11.1...v16.11.2) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* reject requests admitted after their session was abandoned ([#1229](https://github.com/appium/WebDriverAgent/issues/1229)) ([d6b4862](https://github.com/appium/WebDriverAgent/commit/d6b4862ea113083959a80e87c488fd555cbc8061))
|
|
6
|
+
|
|
7
|
+
## [16.11.1](https://github.com/appium/WebDriverAgent/compare/v16.11.0...v16.11.1) (2026-08-29)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* build of FBMjpegServer ([#1239](https://github.com/appium/WebDriverAgent/issues/1239)) ([a3b8650](https://github.com/appium/WebDriverAgent/commit/a3b8650670b9b30c5fc0cd5aa4834a6774d4a828))
|
|
12
|
+
|
|
1
13
|
## [16.11.0](https://github.com/appium/WebDriverAgent/compare/v16.10.0...v16.11.0) (2026-08-29)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -1191,6 +1191,7 @@
|
|
|
1191
1191
|
EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
|
1192
1192
|
EE9AB8011CAEE048008C271F /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; };
|
|
1193
1193
|
EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76571CF7987300275851 /* FBRouteTests.m */; };
|
|
1194
|
+
C312172CE7EE9B10B7A3034B /* FBHTTPServerSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FB741D78562232A78902B374 /* FBHTTPServerSessionTests.m */; };
|
|
1194
1195
|
DF82F4A8758B79DD91FA20CC /* FBHTTPServerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */; };
|
|
1195
1196
|
EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76831CF7997600275851 /* AppDelegate.m */; };
|
|
1196
1197
|
EE9B768F1CF7997600275851 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76851CF7997600275851 /* ViewController.m */; };
|
|
@@ -1908,6 +1909,7 @@
|
|
|
1908
1909
|
EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1909
1910
|
EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1910
1911
|
EE9B76571CF7987300275851 /* FBRouteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBRouteTests.m; sourceTree = "<group>"; };
|
|
1912
|
+
FB741D78562232A78902B374 /* FBHTTPServerSessionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBHTTPServerSessionTests.m; sourceTree = "<group>"; };
|
|
1911
1913
|
76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBHTTPServerTests.m; sourceTree = "<group>"; };
|
|
1912
1914
|
EE9B76581CF7987300275851 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
1913
1915
|
EE9B76821CF7997600275851 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
|
@@ -2679,6 +2681,7 @@
|
|
|
2679
2681
|
713352FC26CEF31D00523CBC /* FBLRUCacheTests.m */,
|
|
2680
2682
|
EE18883C1DA663EB00307AA8 /* FBMathUtilsTests.m */,
|
|
2681
2683
|
718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */,
|
|
2684
|
+
FB741D78562232A78902B374 /* FBHTTPServerSessionTests.m */,
|
|
2682
2685
|
76B7399DDF52C85A21433C1D /* FBHTTPServerTests.m */,
|
|
2683
2686
|
EE9B76571CF7987300275851 /* FBRouteTests.m */,
|
|
2684
2687
|
EE3F8CFD1D08AA17006F02CE /* FBRunLoopSpinnerTests.m */,
|
|
@@ -4816,6 +4819,7 @@
|
|
|
4816
4819
|
719FF5B91DAD21F5008E0099 /* FBElementUtilitiesTests.m in Sources */,
|
|
4817
4820
|
716E0BD11E917F260087A825 /* FBXMLSafeStringTests.m in Sources */,
|
|
4818
4821
|
ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */,
|
|
4822
|
+
C312172CE7EE9B10B7A3034B /* FBHTTPServerSessionTests.m in Sources */,
|
|
4819
4823
|
DF82F4A8758B79DD91FA20CC /* FBHTTPServerTests.m in Sources */,
|
|
4820
4824
|
EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */,
|
|
4821
4825
|
7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */,
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.11.
|
|
18
|
+
<string>16.11.2</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.11.
|
|
22
|
+
<string>16.11.2</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -131,6 +131,10 @@ static BOOL FBParseContentLength(NSString *value, NSUInteger *outLength)
|
|
|
131
131
|
// standalone or not (except DELETE /session itself - see -dispatchMethod:). See
|
|
132
132
|
// -abandonPendingRequestsForSessionID:. Guarded by @synchronized(self.pendingSessionRequests).
|
|
133
133
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<FBPendingRequest *> *> *pendingSessionRequests;
|
|
134
|
+
// Already-abandoned sessions mapped to the response they were abandoned with, so a request parsed
|
|
135
|
+
// after that point is answered at once instead of queueing for a session that is gone. Kept for
|
|
136
|
+
// the server's lifetime; ids are UUIDs. Guarded by @synchronized(self.pendingSessionRequests).
|
|
137
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, RouteResponse *> *abandonedSessionResponses;
|
|
134
138
|
// When each connection started waiting for its current request. The reaper closes connections
|
|
135
139
|
// whose entry outlives FBIncompleteRequestTimeout; idle keep-alive connections have no entry and
|
|
136
140
|
// are exempt. Guarded by @synchronized(self.connectionBuffers).
|
|
@@ -159,6 +163,7 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
|
159
163
|
_connectionsAwaitingResponse = [NSMutableSet set];
|
|
160
164
|
_standaloneWaiters = [NSMutableDictionary dictionary];
|
|
161
165
|
_pendingSessionRequests = [NSMutableDictionary dictionary];
|
|
166
|
+
_abandonedSessionResponses = [NSMutableDictionary dictionary];
|
|
162
167
|
_incompleteRequestStarts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
163
168
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
164
169
|
}
|
|
@@ -678,7 +683,11 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
|
678
683
|
FBPendingRequest *pendingRequest = nil;
|
|
679
684
|
if (nil != sessionID) {
|
|
680
685
|
pendingRequest = [[FBPendingRequest alloc] initWithClient:client];
|
|
681
|
-
[self trackPendingRequest:pendingRequest forSessionID:sessionID];
|
|
686
|
+
RouteResponse *abandonedResponse = [self trackPendingRequest:pendingRequest forSessionID:sessionID];
|
|
687
|
+
if (nil != abandonedResponse) {
|
|
688
|
+
[self writeResponse:abandonedResponse toClient:client];
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
682
691
|
}
|
|
683
692
|
|
|
684
693
|
void (^invoke)(void) = ^{
|
|
@@ -709,15 +718,22 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
|
709
718
|
|
|
710
719
|
#pragma mark - Session-scoped request cancellation
|
|
711
720
|
|
|
712
|
-
|
|
721
|
+
// Returns nil once `pendingRequest` is tracked, or the response an already-abandoned session was
|
|
722
|
+
// abandoned with, which the caller must deliver instead of dispatching.
|
|
723
|
+
- (nullable RouteResponse *)trackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID
|
|
713
724
|
{
|
|
714
725
|
@synchronized (self.pendingSessionRequests) {
|
|
726
|
+
RouteResponse *abandonedResponse = self.abandonedSessionResponses[sessionID];
|
|
727
|
+
if (nil != abandonedResponse) {
|
|
728
|
+
return abandonedResponse;
|
|
729
|
+
}
|
|
715
730
|
NSMutableSet<FBPendingRequest *> *pendingRequests = self.pendingSessionRequests[sessionID];
|
|
716
731
|
if (nil == pendingRequests) {
|
|
717
732
|
pendingRequests = [NSMutableSet set];
|
|
718
733
|
self.pendingSessionRequests[sessionID] = pendingRequests;
|
|
719
734
|
}
|
|
720
735
|
[pendingRequests addObject:pendingRequest];
|
|
736
|
+
return nil;
|
|
721
737
|
}
|
|
722
738
|
}
|
|
723
739
|
|
|
@@ -744,6 +760,8 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
|
744
760
|
@synchronized (self.pendingSessionRequests) {
|
|
745
761
|
pendingRequests = [self.pendingSessionRequests[sessionID] copy];
|
|
746
762
|
[self.pendingSessionRequests removeObjectForKey:sessionID];
|
|
763
|
+
// Recorded before the lock is dropped, so requests admitted from here on are rejected.
|
|
764
|
+
self.abandonedSessionResponses[sessionID] = response;
|
|
747
765
|
}
|
|
748
766
|
for (FBPendingRequest *pendingRequest in pendingRequests) {
|
|
749
767
|
[self writeResponse:response toClient:pendingRequest.client];
|
|
@@ -764,7 +782,11 @@ static const int64_t FBStaleConnectionSweepIntervalSec = 10;
|
|
|
764
782
|
NSString *key = [NSString stringWithFormat:@"%@ %@", method, pathAndQuery];
|
|
765
783
|
FBPendingRequest *waiter = [[FBPendingRequest alloc] initWithClient:client];
|
|
766
784
|
if (nil != sessionID) {
|
|
767
|
-
[self trackPendingRequest:waiter forSessionID:sessionID];
|
|
785
|
+
RouteResponse *abandonedResponse = [self trackPendingRequest:waiter forSessionID:sessionID];
|
|
786
|
+
if (nil != abandonedResponse) {
|
|
787
|
+
[self writeResponse:abandonedResponse toClient:client];
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
768
790
|
}
|
|
769
791
|
|
|
770
792
|
BOOL isInFlight = NO;
|
|
@@ -165,7 +165,7 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
165
165
|
continue;
|
|
166
166
|
}
|
|
167
167
|
[self.pendingFrameCounts setObject:@(pendingFrames + 1) forKey:client];
|
|
168
|
-
[self.socket writeData:chunk toClient:client completion:^{
|
|
168
|
+
[self.socket writeData:chunk toClient:client completion:^(BOOL didSucceed) {
|
|
169
169
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
170
170
|
if (nil == strongSelf) {
|
|
171
171
|
return;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#import <XCTest/XCTest.h>
|
|
10
|
+
|
|
11
|
+
#import <arpa/inet.h>
|
|
12
|
+
#import <stdatomic.h>
|
|
13
|
+
#import <sys/socket.h>
|
|
14
|
+
|
|
15
|
+
#import "FBHTTPServer.h"
|
|
16
|
+
|
|
17
|
+
static atomic_int gSessionProbeHits;
|
|
18
|
+
|
|
19
|
+
@interface FBHTTPServerSessionTests : XCTestCase
|
|
20
|
+
@property (nonatomic, strong) FBHTTPServer *server;
|
|
21
|
+
@property (nonatomic, assign) uint16_t port;
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
@implementation FBHTTPServerSessionTests
|
|
25
|
+
|
|
26
|
+
- (void)setUp
|
|
27
|
+
{
|
|
28
|
+
[super setUp];
|
|
29
|
+
atomic_store(&gSessionProbeHits, 0);
|
|
30
|
+
self.server = [FBHTTPServer new];
|
|
31
|
+
[self.server get:@"/session/:sessionID/probe" withBlock:^(RouteRequest *request, RouteResponse *response) {
|
|
32
|
+
atomic_fetch_add(&gSessionProbeHits, 1);
|
|
33
|
+
[response respondWithString:@"session-probe-ok"];
|
|
34
|
+
}];
|
|
35
|
+
self.server.port = 0;
|
|
36
|
+
NSError *error;
|
|
37
|
+
XCTAssertTrue([self.server start:&error], @"%@", error);
|
|
38
|
+
self.port = [[self.server valueForKeyPath:@"socket.port"] unsignedShortValue];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (void)tearDown
|
|
42
|
+
{
|
|
43
|
+
[self.server stop:NO];
|
|
44
|
+
self.server = nil;
|
|
45
|
+
[super tearDown];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (NSString *)responseForRawPayload:(NSData *)payload timeout:(NSTimeInterval)timeout
|
|
49
|
+
{
|
|
50
|
+
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
51
|
+
if (fd < 0) {
|
|
52
|
+
return nil;
|
|
53
|
+
}
|
|
54
|
+
int noSigpipe = 1;
|
|
55
|
+
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &noSigpipe, sizeof(noSigpipe));
|
|
56
|
+
struct timeval tv = { .tv_sec = (long)timeout, .tv_usec = 0 };
|
|
57
|
+
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
58
|
+
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(self.port) };
|
|
59
|
+
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
60
|
+
if (0 != connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
|
61
|
+
close(fd);
|
|
62
|
+
return nil;
|
|
63
|
+
}
|
|
64
|
+
send(fd, payload.bytes, payload.length, 0);
|
|
65
|
+
NSMutableData *received = [NSMutableData data];
|
|
66
|
+
char chunk[4096];
|
|
67
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
|
|
68
|
+
while (deadline.timeIntervalSinceNow > 0) {
|
|
69
|
+
ssize_t n = recv(fd, chunk, sizeof(chunk), 0);
|
|
70
|
+
if (n > 0) {
|
|
71
|
+
[received appendBytes:chunk length:(NSUInteger)n];
|
|
72
|
+
// The server keeps the connection open after a success, so don't wait the full timeout
|
|
73
|
+
// for an EOF that never comes.
|
|
74
|
+
struct timeval drainTv = { .tv_sec = 0, .tv_usec = 200000 };
|
|
75
|
+
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &drainTv, sizeof(drainTv));
|
|
76
|
+
} else {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
close(fd);
|
|
81
|
+
return [[NSString alloc] initWithData:received encoding:NSUTF8StringEncoding] ?: @"";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)testRequestForAlreadyAbandonedSessionIsRejectedImmediately
|
|
85
|
+
{
|
|
86
|
+
// A request parsed *after* DELETE /session tore the session down never receives an abandonment
|
|
87
|
+
// notification of its own, so before this was tracked it queued on the route queue -
|
|
88
|
+
// potentially forever, if that queue is wedged behind the very request that made the client
|
|
89
|
+
// delete the session in the first place.
|
|
90
|
+
RouteResponse *abandonedResponse = [RouteResponse new];
|
|
91
|
+
[abandonedResponse respondWithString:@"session-was-deleted"];
|
|
92
|
+
[self.server abandonPendingRequestsForSessionID:@"dead-session" withResponse:abandonedResponse];
|
|
93
|
+
|
|
94
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /session/dead-session/probe HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
|
|
95
|
+
timeout:5.0];
|
|
96
|
+
XCTAssertTrue([response containsString:@"session-was-deleted"], @"%@", response);
|
|
97
|
+
XCTAssertEqual(atomic_load(&gSessionProbeHits), 0, @"the route must not run for a deleted session");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (void)testAbandonedSessionIsRememberedAfterManyLaterAbandonments
|
|
101
|
+
{
|
|
102
|
+
// Abandoned ids are kept for the server's lifetime; evicting them would let a stale request
|
|
103
|
+
// queue on a possibly wedged route queue again, which is the hang this rejection prevents.
|
|
104
|
+
RouteResponse *abandonedResponse = [RouteResponse new];
|
|
105
|
+
[abandonedResponse respondWithString:@"session-was-deleted"];
|
|
106
|
+
[self.server abandonPendingRequestsForSessionID:@"dead-session" withResponse:abandonedResponse];
|
|
107
|
+
for (NSUInteger index = 0; index < 64; ++index) {
|
|
108
|
+
RouteResponse *otherResponse = [RouteResponse new];
|
|
109
|
+
[otherResponse respondWithString:@"other-session-was-deleted"];
|
|
110
|
+
[self.server abandonPendingRequestsForSessionID:[NSString stringWithFormat:@"other-session-%lu", (unsigned long)index]
|
|
111
|
+
withResponse:otherResponse];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /session/dead-session/probe HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
|
|
115
|
+
timeout:5.0];
|
|
116
|
+
XCTAssertTrue([response containsString:@"session-was-deleted"], @"%@", response);
|
|
117
|
+
XCTAssertEqual(atomic_load(&gSessionProbeHits), 0, @"the route must not run for a deleted session");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
- (void)testRequestForLiveSessionIsStillServed
|
|
121
|
+
{
|
|
122
|
+
// The rejection above must be scoped to the abandoned identifier only.
|
|
123
|
+
RouteResponse *abandonedResponse = [RouteResponse new];
|
|
124
|
+
[abandonedResponse respondWithString:@"session-was-deleted"];
|
|
125
|
+
[self.server abandonPendingRequestsForSessionID:@"dead-session" withResponse:abandonedResponse];
|
|
126
|
+
|
|
127
|
+
NSString *response = [self responseForRawPayload:(NSData * _Nonnull)[@"GET /session/live-session/probe HTTP/1.1\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]
|
|
128
|
+
timeout:5.0];
|
|
129
|
+
XCTAssertTrue([response containsString:@"session-probe-ok"], @"%@", response);
|
|
130
|
+
XCTAssertEqual(atomic_load(&gSessionProbeHits), 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@end
|