appium-webdriveragent 16.7.3 → 16.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## [16.7.3](https://github.com/appium/WebDriverAgent/compare/v16.7.2...v16.7.3) (2026-08-24)
2
8
 
3
9
  ### Bug Fixes
@@ -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
  }
@@ -309,6 +309,9 @@
309
309
  if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
310
310
  FBConfiguration.sharedInstance.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue];
311
311
  }
312
+ if (nil != capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE]) {
313
+ FBConfiguration.sharedInstance.accessibilityDeadline = [capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE] doubleValue];
314
+ }
312
315
  if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] ||
313
316
  [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) {
314
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.3</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.3</string>
22
+ <string>16.8.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -234,6 +234,17 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
234
234
  */
235
235
  @property (atomic, assign) NSTimeInterval animationCoolOffTimeout;
236
236
 
237
+ /**
238
+ * Maximum time to wait for the frontmost application to confirm its main run loop
239
+ * is responsive before an accessibility snapshot request (element attribute
240
+ * lookups, active app detection, etc). XCTest has no bounded timeout of its own
241
+ * here, so a frozen app could otherwise block WDA forever (#1210); past this
242
+ * timeout the request is aborted with an error instead.
243
+ * Set to zero or negative to disable, restoring unbounded behavior. Disabled (0)
244
+ * by default.
245
+ */
246
+ @property (atomic, assign) NSTimeInterval accessibilityDeadline;
247
+
237
248
  /**
238
249
  Custom class chain locator for accept alert button location.
239
250
  This might be useful if the default buttons detection algorithm fails to determine alert buttons properly
@@ -351,6 +351,7 @@ static NSString *const axSettingsClassName = @"AXSettings";
351
351
  self.autoClickAlertSelector = @"";
352
352
  self.waitForIdleTimeout = 10.;
353
353
  self.animationCoolOffTimeout = 2.;
354
+ self.accessibilityDeadline = 0.;
354
355
  // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100.
355
356
  FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50);
356
357
  FBSetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey, @INT_MAX);
@@ -23,6 +23,7 @@ extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION;
23
23
  extern NSString* const FB_SETTING_KEYBOARD_PREDICTION;
24
24
  extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH;
25
25
  extern NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN;
26
+ extern NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE;
26
27
  extern NSString* const FB_SETTING_USE_FIRST_MATCH;
27
28
  extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX;
28
29
  extern NSString* const FB_SETTING_REDUCE_MOTION;
@@ -19,6 +19,7 @@ NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection";
19
19
  NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction";
20
20
  NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth";
21
21
  NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN = @"snapshotMaxChildren";
22
+ NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE = @"accessibilityDeadline";
22
23
  NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch";
23
24
  NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex";
24
25
  NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion";
@@ -142,6 +142,10 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
142
142
  FBConfiguration.sharedInstance.animationCoolOffTimeout = [value doubleValue];
143
143
  return nil;
144
144
  };
145
+ map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^FBCommandStatus *(FBSession *session, id value) {
146
+ FBConfiguration.sharedInstance.accessibilityDeadline = [value doubleValue];
147
+ return nil;
148
+ };
145
149
  map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^FBCommandStatus *(FBSession *session, id value) {
146
150
  if (nil == value) {
147
151
  session.defaultAlertAction = nil;
@@ -249,6 +253,9 @@ static NSSet<NSString *> *FBNilClearableSettingKeys(void)
249
253
  map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^id(FBSession *session) {
250
254
  return @(FBConfiguration.sharedInstance.animationCoolOffTimeout);
251
255
  };
256
+ map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^id(FBSession *session) {
257
+ return @(FBConfiguration.sharedInstance.accessibilityDeadline);
258
+ };
252
259
  map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^id(FBSession *session) {
253
260
  return @(FBConfiguration.sharedInstance.boundElementsByIndex);
254
261
  };
@@ -38,6 +38,15 @@ NS_ASSUME_NONNULL_BEGIN
38
38
  - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application
39
39
  reply:(void (^)(void))reply;
40
40
 
41
+ /**
42
+ Wraps the private -[XCAXClient_iOS notifyWhenEventLoopIsIdleForApplication:reply:],
43
+ used to check run loop responsiveness before a snapshot request (#1210).
44
+ `reply` may fire more than once per call; `error` is non-nil only if monitoring
45
+ itself could not be started.
46
+ */
47
+ - (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application
48
+ reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply;
49
+
41
50
  - (nullable NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
42
51
  attributes:(NSArray *)attributes
43
52
  error:(NSError**)error;
@@ -81,6 +81,12 @@ static id FBAXClient = nil;
81
81
  [FBAXClient notifyWhenNoAnimationsAreActiveForApplication:application reply:reply];
82
82
  }
83
83
 
84
+ - (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application
85
+ reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply
86
+ {
87
+ [FBAXClient notifyWhenEventLoopIsIdleForApplication:application reply:reply];
88
+ }
89
+
84
90
  - (NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
85
91
  attributes:(NSArray *)attributes
86
92
  error:(NSError**)error;
@@ -92,28 +98,30 @@ static id FBAXClient = nil;
92
98
 
93
99
  - (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid
94
100
  {
95
- NSMutableSet *terminatedAppIds = [NSMutableSet set];
96
- for (NSNumber *appPid in self.appsCache) {
97
- if (![self.appsCache[appPid] running]) {
98
- [terminatedAppIds addObject:appPid];
101
+ @synchronized (self) {
102
+ NSMutableSet *terminatedAppIds = [NSMutableSet set];
103
+ for (NSNumber *appPid in self.appsCache) {
104
+ if (![self.appsCache[appPid] running]) {
105
+ [terminatedAppIds addObject:appPid];
106
+ }
107
+ }
108
+ for (NSNumber *appPid in terminatedAppIds) {
109
+ [self.appsCache removeObjectForKey:appPid];
99
110
  }
100
- }
101
- for (NSNumber *appPid in terminatedAppIds) {
102
- [self.appsCache removeObjectForKey:appPid];
103
- }
104
111
 
105
- XCUIApplication *result = [self.appsCache objectForKey:@(pid)];
106
- if (nil != result) {
107
- return result;
108
- }
112
+ XCUIApplication *result = [self.appsCache objectForKey:@(pid)];
113
+ if (nil != result) {
114
+ return result;
115
+ }
109
116
 
110
- XCUIApplication *app = [[FBAXClient applicationProcessTracker]
111
- monitoredApplicationWithProcessIdentifier:pid];
112
- if (nil == app) {
113
- return nil;
117
+ XCUIApplication *app = [[FBAXClient applicationProcessTracker]
118
+ monitoredApplicationWithProcessIdentifier:pid];
119
+ if (nil == app) {
120
+ return nil;
121
+ }
122
+ [self.appsCache setObject:app forKey:@(pid)];
123
+ return app;
114
124
  }
115
- [self.appsCache setObject:app forKey:@(pid)];
116
- return app;
117
125
  }
118
126
 
119
127
  @end
@@ -38,9 +38,10 @@
38
38
 
39
39
  - (IBAction)deadlockApp:(id)sender
40
40
  {
41
- dispatch_sync(dispatch_get_main_queue(), ^{
42
- // This will never execute
43
- });
41
+ // A self dispatch_sync would trip the OS watchdog and get the process
42
+ // killed outright. Sleeping instead simulates an app that stops answering
43
+ // accessibility requests while staying alive, per #1210.
44
+ [NSThread sleepForTimeInterval:20.0];
44
45
  }
45
46
 
46
47
  - (IBAction)didTapButton:(UIButton *)button
@@ -38,7 +38,6 @@
38
38
  <state key="normal" title="Deadlock app"/>
39
39
  <connections>
40
40
  <action selector="deadlockApp:" destination="BYZ-38-t0r" eventType="touchUpInside" id="53X-DJ-KNY"/>
41
- <action selector="showAlert:" destination="BYZ-38-t0r" eventType="touchUpInside" id="FEN-VX-MMc"/>
42
41
  </connections>
43
42
  </button>
44
43
  <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M2N-Yn-ytb">
@@ -11,6 +11,9 @@
11
11
 
12
12
  #import "FBConfiguration.h"
13
13
  #import "FBRuntimeUtils.h"
14
+ #import "FBTestMacros.h"
15
+ #import "XCUIElement.h"
16
+ #import "XCUIElement+FBIsVisible.h"
14
17
 
15
18
  @interface FBConfigurationTests : FBIntegrationTestCase
16
19
 
@@ -35,4 +38,35 @@
35
38
  XCTAssertEqual(FBConfiguration.sharedInstance.reduceMotionEnabled, defaultReduceMotionEnabled);
36
39
  }
37
40
 
41
+ - (void)testAccessibilityDeadlineAbortsSnapshotRequestForDeadlockedApp
42
+ {
43
+ if (nil != NSProcessInfo.processInfo.environment[@"CI"]) {
44
+ XCTSkip(@"Deliberately freezes the app for several seconds, too slow/flaky for CI");
45
+ }
46
+
47
+ NSTimeInterval previousDeadline = FBConfiguration.sharedInstance.accessibilityDeadline;
48
+ // Also bounds any snapshot-based wait -tap itself may perform once the app is stuck.
49
+ FBConfiguration.sharedInstance.accessibilityDeadline = 3.0;
50
+ @try {
51
+ XCUIElement *deadlockButton = self.testedApplication.buttons[@"Deadlock app"];
52
+ FBAssertWaitTillBecomesTrue(deadlockButton.fb_isVisible);
53
+ // Freezes the app's main thread for 20s - see -[ViewController deadlockApp:].
54
+ [deadlockButton tap];
55
+
56
+ NSError *error;
57
+ NSDate *start = [NSDate date];
58
+ id snapshot = [self.testedApplication snapshotWithError:&error];
59
+ NSTimeInterval elapsed = -start.timeIntervalSinceNow;
60
+
61
+ XCTAssertNil(snapshot);
62
+ XCTAssertNotNil(error);
63
+ // Should abort close to accessibilityDeadline (plus XCTest's own internal
64
+ // retries), not hang indefinitely waiting for the frozen app (#1210).
65
+ XCTAssertLessThan(elapsed, 20.0);
66
+ } @finally {
67
+ FBConfiguration.sharedInstance.accessibilityDeadline = previousDeadline;
68
+ [self.testedApplication terminate];
69
+ }
70
+ }
71
+
38
72
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.7.3",
3
+ "version": "16.8.0",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",