appium-webdriveragent 16.7.2 → 16.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +0 -10
  3. package/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +90 -0
  4. package/WebDriverAgentLib/Commands/FBCustomCommands.m +4 -1
  5. package/WebDriverAgentLib/Commands/FBScreenshotCommands.m +2 -2
  6. package/WebDriverAgentLib/Commands/FBSessionCommands.m +6 -5
  7. package/WebDriverAgentLib/Info.plist +2 -2
  8. package/WebDriverAgentLib/Routing/FBHTTPServer.h +25 -1
  9. package/WebDriverAgentLib/Routing/FBHTTPServer.m +278 -92
  10. package/WebDriverAgentLib/Routing/FBRoute.h +9 -0
  11. package/WebDriverAgentLib/Routing/FBRoute.m +9 -0
  12. package/WebDriverAgentLib/Routing/FBSession.h +13 -0
  13. package/WebDriverAgentLib/Routing/FBSession.m +153 -22
  14. package/WebDriverAgentLib/Routing/FBTCPSocket.m +2 -1
  15. package/WebDriverAgentLib/Routing/FBWebServer.m +26 -1
  16. package/WebDriverAgentLib/Routing/RouteResponse.m +1 -1
  17. package/WebDriverAgentLib/Utilities/FBConfiguration.h +11 -0
  18. package/WebDriverAgentLib/Utilities/FBConfiguration.m +8 -10
  19. package/WebDriverAgentLib/Utilities/FBSettings.h +1 -0
  20. package/WebDriverAgentLib/Utilities/FBSettings.m +1 -0
  21. package/WebDriverAgentLib/Utilities/FBSettingsHandler.m +7 -0
  22. package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +9 -0
  23. package/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +26 -18
  24. package/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +10 -7
  25. package/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +36 -15
  26. package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +4 -3
  27. package/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +0 -1
  28. package/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +34 -0
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [16.8.0](https://github.com/appium/WebDriverAgent/compare/v16.7.3...v16.8.0) (2026-08-24)
2
+
3
+ ### Features
4
+
5
+ * bound accessibility snapshot requests to avoid indefinite hangs ([#1214](https://github.com/appium/WebDriverAgent/issues/1214)) ([cd829eb](https://github.com/appium/WebDriverAgent/commit/cd829eb9725f57efbfb9538a073de059020ddd38))
6
+
7
+ ## [16.7.3](https://github.com/appium/WebDriverAgent/compare/v16.7.2...v16.7.3) (2026-08-24)
8
+
9
+ ### Bug Fixes
10
+
11
+ * 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))
12
+
1
13
  ## [16.7.2](https://github.com/appium/WebDriverAgent/compare/v16.7.1...v16.7.2) (2026-08-24)
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!
@@ -10,6 +10,13 @@
10
10
 
11
11
  #import <objc/runtime.h>
12
12
 
13
+ #import "FBConfiguration.h"
14
+ #import "FBErrorBuilder.h"
15
+ #import "FBLogger.h"
16
+ #import "FBXCAccessibilityElement.h"
17
+ #import "FBXCAXClientProxy.h"
18
+ #import "XCUIApplication.h"
19
+
13
20
  /**
14
21
  Available parameters with their default values for XCTest:
15
22
  @"maxChildren" : (int)2147483647
@@ -60,6 +67,82 @@ static id swizzledSnapshotParameters(id self, SEL _cmd)
60
67
  return result;
61
68
  }
62
69
 
70
+ static id (*original_requestSnapshotForElement)(id, SEL, id, id, id, NSError **);
71
+
72
+ // pid -> last-unresponsive-at. XCTest retries a failed snapshot request several
73
+ // times in a row; this lets retries fail fast within `timeout` of the last check
74
+ // instead of each re-running the full wait.
75
+ static NSMutableDictionary<NSNumber *, NSDate *> *unresponsiveApplicationPids;
76
+ static NSObject *unresponsiveApplicationPidsLock;
77
+
78
+ static NSError *FBBuildUnresponsiveApplicationError(int pid, NSTimeInterval timeout)
79
+ {
80
+ // https://github.com/appium/WebDriverAgent/issues/1210
81
+ NSString *description = [NSString stringWithFormat:
82
+ @"The application with process identifier %d did not confirm its main run loop is "
83
+ @"responsive within %.1f second(s) and is likely in an unresponsive state. "
84
+ @"Aborting the accessibility snapshot request instead of risking an indefinite "
85
+ @"hang.",
86
+ pid, timeout];
87
+ [FBLogger logFmt:@"%@", description];
88
+ NSError *error;
89
+ [[[FBErrorBuilder builder] withDescription:description] buildError:&error];
90
+ return error;
91
+ }
92
+
93
+ // Guards -[XCAXClient_iOS requestSnapshotForElement:...] against hanging forever
94
+ // on an unresponsive app (#1210). If accessibilityDeadline > 0, checks run loop
95
+ // responsiveness first and aborts with an error instead of risking an unbounded
96
+ // wait; otherwise falls through to the original, unbounded behavior.
97
+ static id swizzledRequestSnapshotForElement(id self, SEL _cmd, id element, id attributes, id parameters, NSError **error)
98
+ {
99
+ NSTimeInterval timeout = FBConfiguration.sharedInstance.accessibilityDeadline;
100
+ if (timeout < DBL_EPSILON) {
101
+ return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
102
+ }
103
+
104
+ int pid = [(id<FBXCAccessibilityElement>)element processIdentifier];
105
+ XCUIApplication *application = [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:pid];
106
+ if (nil == application) {
107
+ // Nothing to confirm responsiveness for (e.g. the system element) - fall
108
+ // through to the original behavior.
109
+ return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
110
+ }
111
+
112
+ NSNumber *pidKey = @(pid);
113
+ @synchronized (unresponsiveApplicationPidsLock) {
114
+ NSDate *markedUnresponsiveAt = unresponsiveApplicationPids[pidKey];
115
+ if (nil != markedUnresponsiveAt && -markedUnresponsiveAt.timeIntervalSinceNow < timeout) {
116
+ if (nil != error) {
117
+ *error = FBBuildUnresponsiveApplicationError(pid, timeout);
118
+ }
119
+ return nil;
120
+ }
121
+ }
122
+
123
+ dispatch_semaphore_t sem = dispatch_semaphore_create(0);
124
+ __block BOOL isResponsive = NO;
125
+ [FBXCAXClientProxy.sharedClient notifyWhenEventLoopIsIdleForApplication:application
126
+ reply:^(id result, NSError *idleError) {
127
+ isResponsive = (nil == idleError);
128
+ dispatch_semaphore_signal(sem);
129
+ }];
130
+ BOOL didReplyInTime = 0 == dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)));
131
+ if (didReplyInTime && isResponsive) {
132
+ @synchronized (unresponsiveApplicationPidsLock) {
133
+ [unresponsiveApplicationPids removeObjectForKey:pidKey];
134
+ }
135
+ return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
136
+ }
137
+
138
+ @synchronized (unresponsiveApplicationPidsLock) {
139
+ unresponsiveApplicationPids[pidKey] = [NSDate date];
140
+ }
141
+ if (nil != error) {
142
+ *error = FBBuildUnresponsiveApplicationError(pid, timeout);
143
+ }
144
+ return nil;
145
+ }
63
146
 
64
147
  @implementation XCAXClient_iOS (FBSnapshotReqParams)
65
148
 
@@ -69,6 +152,9 @@ static id swizzledSnapshotParameters(id self, SEL _cmd)
69
152
 
70
153
  + (void)load
71
154
  {
155
+ unresponsiveApplicationPids = [NSMutableDictionary new];
156
+ unresponsiveApplicationPidsLock = [NSObject new];
157
+
72
158
  Method original_defaultParametersMethod = class_getInstanceMethod(self.class, @selector(defaultParameters));
73
159
  IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters;
74
160
  original_defaultParameters = (id (*)(id, SEL)) method_setImplementation(original_defaultParametersMethod, swizzledDefaultParametersImp);
@@ -76,6 +162,10 @@ static id swizzledSnapshotParameters(id self, SEL _cmd)
76
162
  Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters"));
77
163
  IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters;
78
164
  original_snapshotParameters = (id (*)(id, SEL)) method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp);
165
+
166
+ Method original_requestSnapshotForElementMethod = class_getInstanceMethod(self.class, @selector(requestSnapshotForElement:attributes:parameters:error:));
167
+ IMP swizzledRequestSnapshotForElementImp = (IMP)swizzledRequestSnapshotForElement;
168
+ original_requestSnapshotForElement = (id (*)(id, SEL, id, id, id, NSError **)) method_setImplementation(original_requestSnapshotForElementMethod, swizzledRequestSnapshotForElementImp);
79
169
  }
80
170
 
81
171
  #pragma clang diagnostic pop
@@ -205,10 +205,13 @@
205
205
  + (id<FBResponsePayload>)handleActiveAppInfo:(FBRouteRequest *)request
206
206
  {
207
207
  XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
208
+ // .identifier can be nil if the app stopped answering accessibility requests
209
+ // and accessibilityDeadline aborted the underlying snapshot fetch (#1210).
210
+ NSString *name = app.identifier ?: @"unknown";
208
211
  return FBResponseWithObject(@{
209
212
  @"pid": @(app.processID),
210
213
  @"bundleId": app.bundleID,
211
- @"name": app.identifier,
214
+ @"name": name,
212
215
  @"processArguments": [self processArguments:app],
213
216
  });
214
217
  }
@@ -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
- if (nil != FBSession.activeSession) {
93
- [FBSession.activeSession kill];
94
- }
92
+ [FBSession killActiveSessionAndWaitForTeardown];
95
93
 
96
94
  NSDictionary<NSString *, id> *capabilities;
97
95
  id<FBResponsePayload> errorResponse = [self capabilitiesFromCreateSessionRequest:request
@@ -311,6 +309,9 @@
311
309
  if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
312
310
  FBConfiguration.sharedInstance.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue];
313
311
  }
312
+ if (nil != capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE]) {
313
+ FBConfiguration.sharedInstance.accessibilityDeadline = [capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE] doubleValue];
314
+ }
314
315
  if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] ||
315
316
  [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) {
316
317
  [FBConfiguration.sharedInstance forceSimulatorSoftwareKeyboardPresence];
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.7.2</string>
18
+ <string>16.8.0</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.7.2</string>
22
+ <string>16.8.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -47,17 +47,41 @@ NS_ASSUME_NONNULL_BEGIN
47
47
 
48
48
  /**
49
49
  Registers a route handler for the given HTTP method and path pattern (":param" segments are
50
- captured into the request's `params`).
50
+ captured into the request's `params`). Equivalent to -handleMethod:withPath:standalone:block:
51
+ with standalone:NO.
51
52
  */
52
53
  - (void)handleMethod:(NSString *)method
53
54
  withPath:(NSString *)path
54
55
  block:(void (^)(RouteRequest *request, RouteResponse *response))block;
55
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
+
56
69
  /**
57
70
  Convenience for -handleMethod:@"GET" withPath:path block:block.
58
71
  */
59
72
  - (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block;
60
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
+
61
85
  /**
62
86
  Starts listening on `port`.
63
87
  */