appium-webdriveragent 16.11.3 → 16.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h +9 -0
- package/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m +28 -12
- package/WebDriverAgentLib/Routing/FBSession.h +3 -0
- package/WebDriverAgentLib/Routing/FBSession.m +109 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [16.11.4](https://github.com/appium/WebDriverAgent/compare/v16.11.3...v16.11.4) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* prevent a stale teardown from affecting a newer session generation ([#1231](https://github.com/appium/WebDriverAgent/issues/1231)) ([83642a1](https://github.com/appium/WebDriverAgent/commit/83642a11218fd2b2801af0a04d3b253125246bd7))
|
|
6
|
+
|
|
1
7
|
## [16.11.3](https://github.com/appium/WebDriverAgent/compare/v16.11.2...v16.11.3) (2026-08-30)
|
|
2
8
|
|
|
3
9
|
### Miscellaneous Chores
|
|
@@ -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.4</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.11.
|
|
22
|
+
<string>16.11.4</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -44,6 +44,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
44
44
|
*/
|
|
45
45
|
- (void)reset;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
Resets the container, but only if it still keeps the given promise. The comparison and the
|
|
49
|
+
reset are performed atomically, so a promise stored concurrently is never dropped.
|
|
50
|
+
|
|
51
|
+
@param screenRecordingPromise the promise the caller expects to be still active
|
|
52
|
+
@return YES if the container has been reset
|
|
53
|
+
*/
|
|
54
|
+
- (BOOL)resetIfPromiseIs:(FBScreenRecordingPromise *)screenRecordingPromise;
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
Transforms the container content to a dictionary.
|
|
49
58
|
|
|
@@ -36,23 +36,39 @@
|
|
|
36
36
|
fps:(NSUInteger)fps
|
|
37
37
|
codec:(long long)codec;
|
|
38
38
|
{
|
|
39
|
-
self
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
@synchronized (self) {
|
|
40
|
+
self.fps = fps;
|
|
41
|
+
self.codec = codec;
|
|
42
|
+
self.screenRecordingPromise = screenRecordingPromise;
|
|
43
|
+
self.startedAt = @([NSDate.date timeIntervalSince1970]);
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
- (void)reset;
|
|
46
48
|
{
|
|
47
|
-
self
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
@synchronized (self) {
|
|
50
|
+
self.fps = 0;
|
|
51
|
+
self.codec = 0;
|
|
52
|
+
if (nil != self.screenRecordingPromise) {
|
|
53
|
+
[XCTContext runActivityNamed:@"Video Cleanup" block:^(id<XCTActivity> activity){
|
|
54
|
+
[activity addAttachment:(XCTAttachment *)self.screenRecordingPromise.nativePromise];
|
|
55
|
+
}];
|
|
56
|
+
self.screenRecordingPromise = nil;
|
|
57
|
+
}
|
|
58
|
+
self.startedAt = nil;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (BOOL)resetIfPromiseIs:(FBScreenRecordingPromise *)screenRecordingPromise
|
|
63
|
+
{
|
|
64
|
+
// @synchronized is recursive, so -reset may take the very same lock again below.
|
|
65
|
+
@synchronized (self) {
|
|
66
|
+
if (self.screenRecordingPromise != screenRecordingPromise) {
|
|
67
|
+
return NO;
|
|
68
|
+
}
|
|
69
|
+
[self reset];
|
|
70
|
+
return YES;
|
|
54
71
|
}
|
|
55
|
-
self.startedAt = nil;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
- (nullable NSDictionary *)toDictionary
|
|
@@ -54,6 +54,9 @@ extern NSString* const FBSessionWasKilledNotification;
|
|
|
54
54
|
Kills the active session, if any, and blocks until its teardown - including one already started
|
|
55
55
|
by a concurrent caller - is fully finished. Call this before preparing/launching a replacement
|
|
56
56
|
application, so it can't race a still-in-progress termination of the outgoing one.
|
|
57
|
+
|
|
58
|
+
@throws FBSessionCreationException if the outgoing application's termination is still in flight
|
|
59
|
+
after the wait, since a replacement must never start while that termination can still land.
|
|
57
60
|
*/
|
|
58
61
|
+ (void)killActiveSessionAndWaitForTeardown;
|
|
59
62
|
|
|
@@ -50,7 +50,7 @@ NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotificatio
|
|
|
50
50
|
@property (nonatomic, readwrite) NSMutableDictionary<NSNumber *, NSMutableDictionary<NSString *, NSNumber *> *> *elementsVisibilityCache;
|
|
51
51
|
|
|
52
52
|
- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout;
|
|
53
|
-
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout;
|
|
53
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout generation:(NSUInteger)generation;
|
|
54
54
|
@end
|
|
55
55
|
|
|
56
56
|
@interface FBSession (FBAlertsMonitorDelegate)
|
|
@@ -100,11 +100,20 @@ NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotificatio
|
|
|
100
100
|
|
|
101
101
|
@implementation FBSession
|
|
102
102
|
|
|
103
|
+
// Guarded, together with the two counters below, by +teardownCondition.
|
|
103
104
|
static FBSession *_activeSession = nil;
|
|
104
105
|
// Class-level, not per-instance: a caller that finds _activeSession already nil (a concurrent
|
|
105
106
|
// -kill beat it there) still needs to know whether that -kill's teardown is done, since it cleared
|
|
106
107
|
// the pointer before running it. See +waitForActiveTeardownWithTimeout:.
|
|
107
|
-
|
|
108
|
+
// A count, not a flag: the bounded wait below lets teardowns overlap, so one of them finishing
|
|
109
|
+
// must not wake waiters while another still runs.
|
|
110
|
+
static NSUInteger _activeTeardownCount = 0;
|
|
111
|
+
// Bumped once a caller owns the device, before it launches anything; a teardown still running past
|
|
112
|
+
// the bounded wait re-checks it before touching process-wide state.
|
|
113
|
+
static NSUInteger _sessionGeneration = 0;
|
|
114
|
+
// Teardowns that have claimed the current generation and are committed to terminating the app.
|
|
115
|
+
// The generation bump waits for these to drain, so a claim and a bump can never interleave.
|
|
116
|
+
static NSUInteger _committedTerminationCount = 0;
|
|
108
117
|
|
|
109
118
|
+ (NSCondition *)teardownCondition
|
|
110
119
|
{
|
|
@@ -116,25 +125,29 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
116
125
|
return condition;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
// Waits (bounded) for
|
|
128
|
+
// Waits (bounded) for every -kill teardown currently in progress to finish.
|
|
120
129
|
+ (void)waitForActiveTeardownWithTimeout:(NSTimeInterval)timeout
|
|
121
130
|
{
|
|
122
131
|
NSCondition *condition = self.teardownCondition;
|
|
123
132
|
[condition lock];
|
|
124
133
|
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout];
|
|
125
|
-
while (
|
|
134
|
+
while (_activeTeardownCount > 0 && [condition waitUntilDate:deadline]) {
|
|
126
135
|
}
|
|
127
136
|
[condition unlock];
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
+ (instancetype)activeSession
|
|
131
140
|
{
|
|
132
|
-
|
|
141
|
+
NSCondition *condition = self.teardownCondition;
|
|
142
|
+
[condition lock];
|
|
143
|
+
FBSession *session = _activeSession;
|
|
144
|
+
[condition unlock];
|
|
145
|
+
return session;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
+ (void)killActiveSessionAndWaitForTeardown
|
|
136
149
|
{
|
|
137
|
-
FBSession *session =
|
|
150
|
+
FBSession *session = self.activeSession;
|
|
138
151
|
if (nil != session) {
|
|
139
152
|
// Runs the real teardown synchronously if this call wins the race in -kill, or waits for
|
|
140
153
|
// whoever did to finish if it lost - either way, blocks until torn down.
|
|
@@ -144,12 +157,69 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
144
157
|
// be mid-teardown - wait for it, so we don't launch a replacement app too early.
|
|
145
158
|
[self waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC];
|
|
146
159
|
}
|
|
160
|
+
// Claimed before the caller launches its replacement app: if the bounded wait expired with a
|
|
161
|
+
// teardown still running, that teardown must be stale by the time the new app exists.
|
|
162
|
+
NSCondition *condition = self.teardownCondition;
|
|
163
|
+
[condition lock];
|
|
164
|
+
// A committed -terminate cannot be revoked, so the next generation must never be handed out
|
|
165
|
+
// while one is in flight - give up on the new session instead of racing it.
|
|
166
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:FB_APP_TERMINATE_TIMEOUT_SEC];
|
|
167
|
+
while (_committedTerminationCount > 0 && [condition waitUntilDate:deadline]) {
|
|
168
|
+
}
|
|
169
|
+
BOOL isTerminationPending = _committedTerminationCount > 0;
|
|
170
|
+
if (!isTerminationPending) {
|
|
171
|
+
_sessionGeneration++;
|
|
172
|
+
}
|
|
173
|
+
[condition unlock];
|
|
174
|
+
if (isTerminationPending) {
|
|
175
|
+
NSString *reason = [NSString stringWithFormat:@"The termination of a previous session's application is still in progress after %@ seconds. Please retry the session creation later", @(FB_APP_TERMINATE_TIMEOUT_SEC)];
|
|
176
|
+
@throw [NSException exceptionWithName:FBSessionCreationException reason:reason userInfo:nil];
|
|
177
|
+
}
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
+ (void)markSessionActive:(FBSession *)session
|
|
150
181
|
{
|
|
151
182
|
[self killActiveSessionAndWaitForTeardown];
|
|
183
|
+
NSCondition *condition = self.teardownCondition;
|
|
184
|
+
[condition lock];
|
|
152
185
|
_activeSession = session;
|
|
186
|
+
[condition unlock];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validates and claims `generation` in one critical section, so no replacement can be handed the
|
|
190
|
+
// next generation until the matching +endTermination. NO means this teardown is already stale.
|
|
191
|
+
+ (BOOL)beginTerminationForGeneration:(NSUInteger)generation
|
|
192
|
+
{
|
|
193
|
+
NSCondition *condition = self.teardownCondition;
|
|
194
|
+
[condition lock];
|
|
195
|
+
BOOL isCurrent = generation == _sessionGeneration;
|
|
196
|
+
if (isCurrent) {
|
|
197
|
+
_committedTerminationCount++;
|
|
198
|
+
}
|
|
199
|
+
[condition unlock];
|
|
200
|
+
return isCurrent;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
+ (void)endTermination
|
|
204
|
+
{
|
|
205
|
+
NSCondition *condition = self.teardownCondition;
|
|
206
|
+
[condition lock];
|
|
207
|
+
_committedTerminationCount--;
|
|
208
|
+
[condition broadcast];
|
|
209
|
+
[condition unlock];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Read in the same critical section that validates the generation: a replacement would have bumped
|
|
213
|
+
// the generation before storing anything, so a promise captured here can never be its.
|
|
214
|
+
+ (FBScreenRecordingPromise *)activeScreenRecordingForGeneration:(NSUInteger)generation
|
|
215
|
+
{
|
|
216
|
+
NSCondition *condition = self.teardownCondition;
|
|
217
|
+
[condition lock];
|
|
218
|
+
FBScreenRecordingPromise *promise = generation == _sessionGeneration
|
|
219
|
+
? FBScreenRecordingContainer.sharedInstance.screenRecordingPromise
|
|
220
|
+
: nil;
|
|
221
|
+
[condition unlock];
|
|
222
|
+
return promise;
|
|
153
223
|
}
|
|
154
224
|
|
|
155
225
|
+ (instancetype)sessionWithIdentifier:(NSString *)identifier
|
|
@@ -157,10 +227,9 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
157
227
|
if (!identifier) {
|
|
158
228
|
return nil;
|
|
159
229
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return _activeSession;
|
|
230
|
+
// A single snapshot: reading the global twice could validate one session and return another.
|
|
231
|
+
FBSession *session = self.activeSession;
|
|
232
|
+
return [identifier isEqualToString:session.identifier] ? session : nil;
|
|
164
233
|
}
|
|
165
234
|
|
|
166
235
|
+ (instancetype)initWithApplication:(XCUIApplication *)application
|
|
@@ -220,13 +289,20 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
220
289
|
// DELETE /session and session creation can now run concurrently, so a session already
|
|
221
290
|
// superseded by a newer one can still reach here via a stale reference. Check-and-clear must be
|
|
222
291
|
// atomic, else a belated -kill could null out the new session's pointer instead of its own.
|
|
292
|
+
NSCondition *teardownCondition = self.class.teardownCondition;
|
|
223
293
|
BOOL wasActive;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
294
|
+
NSUInteger generation;
|
|
295
|
+
[teardownCondition lock];
|
|
296
|
+
wasActive = (self == _activeSession);
|
|
297
|
+
// Captured so the teardown steps below can tell whether a replacement has claimed the device.
|
|
298
|
+
generation = _sessionGeneration;
|
|
299
|
+
if (wasActive) {
|
|
300
|
+
_activeSession = nil;
|
|
301
|
+
// Registered in the same critical section as the clear above, else a concurrent session
|
|
302
|
+
// creation could observe neither an active session nor a teardown and skip its wait.
|
|
303
|
+
_activeTeardownCount++;
|
|
229
304
|
}
|
|
305
|
+
[teardownCondition unlock];
|
|
230
306
|
if (!wasActive) {
|
|
231
307
|
// Someone else is already tearing this session down - wait for that to finish (bounded), so
|
|
232
308
|
// we don't act as if it's gone (e.g. launch a new app) while its -terminate is still in flight.
|
|
@@ -234,24 +310,23 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
234
310
|
return;
|
|
235
311
|
}
|
|
236
312
|
|
|
237
|
-
NSCondition *teardownCondition = self.class.teardownCondition;
|
|
238
|
-
[teardownCondition lock];
|
|
239
|
-
_isTeardownInProgress = YES;
|
|
240
|
-
[teardownCondition unlock];
|
|
241
|
-
|
|
242
313
|
@try {
|
|
243
314
|
// Posted before teardown so pending HTTP requests for this session can stop waiting sooner.
|
|
244
315
|
[NSNotificationCenter.defaultCenter postNotificationName:FBSessionWasKilledNotification object:self];
|
|
245
316
|
|
|
246
317
|
[self disableAlertsMonitor];
|
|
247
318
|
|
|
248
|
-
|
|
319
|
+
// The container is process-wide, so only act on a promise captured while this teardown still
|
|
320
|
+
// owned the generation - nil here means it is stale and must leave the recording alone.
|
|
321
|
+
FBScreenRecordingPromise *activeScreenRecording = [self.class activeScreenRecordingForGeneration:generation];
|
|
249
322
|
if (nil != activeScreenRecording) {
|
|
250
323
|
NSError *error;
|
|
251
324
|
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) {
|
|
252
325
|
[FBLogger logFmt:@"%@", error];
|
|
253
326
|
}
|
|
254
|
-
|
|
327
|
+
// Identity, not generation: the stop above may outlast a replacement storing its own promise.
|
|
328
|
+
// Compare-and-reset, so that replacement's store cannot land between the check and the reset.
|
|
329
|
+
[FBScreenRecordingContainer.sharedInstance resetIfPromiseIs:activeScreenRecording];
|
|
255
330
|
}
|
|
256
331
|
|
|
257
332
|
if (nil != self.testedApplication
|
|
@@ -259,13 +334,14 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
259
334
|
&& self.testedApplication.running
|
|
260
335
|
&& ![self fb_isTestedApplicationSameAsSystemAppWithTimeout:FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC]) {
|
|
261
336
|
// 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
|
|
263
|
-
// teardown as finished as soon as this returns.
|
|
264
|
-
[self fb_terminateTestedApplicationWithTimeout:FB_APP_TERMINATE_TIMEOUT_SEC];
|
|
337
|
+
// pending) - see -fb_terminateTestedApplicationWithTimeout:generation: - so it's safe to
|
|
338
|
+
// report this teardown as finished as soon as this returns.
|
|
339
|
+
[self fb_terminateTestedApplicationWithTimeout:FB_APP_TERMINATE_TIMEOUT_SEC generation:generation];
|
|
265
340
|
}
|
|
266
341
|
} @finally {
|
|
267
342
|
[teardownCondition lock];
|
|
268
|
-
|
|
343
|
+
_activeTeardownCount--;
|
|
344
|
+
// Unconditional: waiters re-check the count, so a wake-up mid-teardown just puts them back.
|
|
269
345
|
[teardownCondition broadcast];
|
|
270
346
|
[teardownCondition unlock];
|
|
271
347
|
}
|
|
@@ -394,7 +470,7 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
394
470
|
// `timeout` - but a "given up on" call must never still terminate whatever's running by the time
|
|
395
471
|
// main gets to it (e.g. a replacement session's app), so cancellation and the actual terminate
|
|
396
472
|
// call share a lock: whichever gets there first - the dispatched block, or the timeout - wins.
|
|
397
|
-
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout
|
|
473
|
+
- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout generation:(NSUInteger)generation
|
|
398
474
|
{
|
|
399
475
|
XCUIApplication *application = self.testedApplication;
|
|
400
476
|
NSObject *lock = [NSObject new];
|
|
@@ -402,11 +478,16 @@ static BOOL _isTeardownInProgress = NO;
|
|
|
402
478
|
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
403
479
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
404
480
|
@synchronized (lock) {
|
|
405
|
-
|
|
481
|
+
// Re-checked here, not before dispatching: this block can sit on a busy main queue past the
|
|
482
|
+
// teardown wait, and a replacement usually runs the same bundle ID as the app to terminate.
|
|
483
|
+
// The claim is held across -terminate, so no replacement can take the next generation mid-call.
|
|
484
|
+
if (isAllowedToTerminate && [self.class beginTerminationForGeneration:generation]) {
|
|
406
485
|
@try {
|
|
407
486
|
[application terminate];
|
|
408
487
|
} @catch (NSException *e) {
|
|
409
488
|
[FBLogger logFmt:@"%@", e.description];
|
|
489
|
+
} @finally {
|
|
490
|
+
[self.class endTermination];
|
|
410
491
|
}
|
|
411
492
|
}
|
|
412
493
|
}
|