appium-uiautomator2-driver 7.5.2 → 7.6.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.
@@ -0,0 +1,292 @@
1
+ import type {AppiumLogger, IAppiumIpc, IIpcSubscription, IpcMessage} from '@appium/types';
2
+ import {node, util} from 'appium/support';
3
+ import {waitForCondition} from 'asyncbox';
4
+ import {setTimeout as delay} from 'node:timers/promises';
5
+ import type {AndroidUiautomator2Driver} from './driver';
6
+ import {memoize} from './utils';
7
+
8
+ export type SessionUdidIpcMessage = {
9
+ udid: string;
10
+ sessionId: string;
11
+ };
12
+
13
+ type AppiumIpcConstructor = new () => IAppiumIpc;
14
+ type IpcProvider = () => Promise<IAppiumIpc | undefined>;
15
+
16
+ export class SessionClaimHandler {
17
+ static readonly CLAIMED_TOPIC = 'uiautomator2:sessionUdidClaimed';
18
+ static readonly CONTENDED_TOPIC = 'uiautomator2:sessionUdidContended';
19
+ static readonly RELEASED_TOPIC = 'uiautomator2:sessionUdidReleased';
20
+
21
+ private static readonly CONTENTION_PROBE_MS = 10;
22
+ private static readonly RELEASE_WAIT_MS = 15000;
23
+
24
+ private readonly subscriptionsBySessionId = new Map<
25
+ string,
26
+ IIpcSubscription<SessionUdidIpcMessage>
27
+ >();
28
+
29
+ constructor(private readonly getIpc: IpcProvider) {}
30
+
31
+ /** Subscribe the current session to udid claim messages from other sessions. */
32
+ async registerActiveSession(driver: AndroidUiautomator2Driver): Promise<void> {
33
+ const ipc = await this.getIpc();
34
+ const udid = driver.opts.udid;
35
+ const sessionId = driver.sessionId;
36
+ if (!ipc || !udid || !sessionId) {
37
+ return;
38
+ }
39
+
40
+ this.unregisterActiveSession(driver);
41
+
42
+ const subscription = ipc.subscribe<SessionUdidIpcMessage>(
43
+ SessionClaimHandler.CLAIMED_TOPIC,
44
+ this.getPublisherId(driver),
45
+ );
46
+ subscription.on('message', (message) => {
47
+ void this.dispatchSessionUdidMessage(driver, udid, sessionId, message);
48
+ });
49
+ this.subscriptionsBySessionId.set(sessionId, subscription);
50
+ }
51
+
52
+ /** Unsubscribe the current session from udid claim messages. */
53
+ unregisterActiveSession(driver: AndroidUiautomator2Driver): void {
54
+ const sessionId = driver.sessionId;
55
+ if (!sessionId) {
56
+ return;
57
+ }
58
+
59
+ this.subscriptionsBySessionId.get(sessionId)?.unsubscribe();
60
+ this.subscriptionsBySessionId.delete(sessionId);
61
+ }
62
+
63
+ /** Publish this session's udid so any existing session on the same device can terminate. */
64
+ async claimSessionUdid(driver: AndroidUiautomator2Driver): Promise<void> {
65
+ const ipc = await this.getIpc();
66
+ if (!ipc) {
67
+ driver.log.debug(
68
+ 'Driver-instance IPC is unavailable. Skipping publication of the session udid.',
69
+ );
70
+ return;
71
+ }
72
+
73
+ const udid = driver.opts.udid;
74
+ const sessionId = driver.sessionId;
75
+ if (!udid || !sessionId) {
76
+ driver.log.debug('The session udid is not known yet. Skipping udid publication.');
77
+ return;
78
+ }
79
+
80
+ const contendingSessionIds = new Set<string>();
81
+ const releasedSessionIds = new Set<string>();
82
+ const contendedSubscription = ipc.subscribe<SessionUdidIpcMessage>(
83
+ SessionClaimHandler.CONTENDED_TOPIC,
84
+ this.getPublisherId(driver),
85
+ );
86
+ const releasedSubscription = ipc.subscribe<SessionUdidIpcMessage>(
87
+ SessionClaimHandler.RELEASED_TOPIC,
88
+ this.getPublisherId(driver),
89
+ );
90
+ contendedSubscription.on('message', (message) => {
91
+ if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
92
+ contendingSessionIds.add(message.data.sessionId);
93
+ }
94
+ });
95
+ releasedSubscription.on('message', (message) => {
96
+ if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
97
+ releasedSessionIds.add(message.data.sessionId);
98
+ }
99
+ });
100
+
101
+ try {
102
+ await ipc.publish<SessionUdidIpcMessage>(
103
+ SessionClaimHandler.CLAIMED_TOPIC,
104
+ this.getPublisherId(driver),
105
+ {
106
+ udid,
107
+ sessionId,
108
+ },
109
+ );
110
+ await delay(SessionClaimHandler.CONTENTION_PROBE_MS);
111
+
112
+ if (contendingSessionIds.size === 0) {
113
+ return;
114
+ }
115
+
116
+ try {
117
+ await waitForCondition(
118
+ () => [...contendingSessionIds].every((id) => releasedSessionIds.has(id)),
119
+ {
120
+ waitMs: SessionClaimHandler.RELEASE_WAIT_MS,
121
+ intervalMs: 50,
122
+ },
123
+ );
124
+ driver.log.debug(
125
+ `Received release confirmation from ` +
126
+ `${util.pluralize('session', contendingSessionIds.size, true)} for udid '${udid}'`,
127
+ );
128
+ } catch {
129
+ const pendingSessionIds = [...contendingSessionIds].filter(
130
+ (id) => !releasedSessionIds.has(id),
131
+ );
132
+ driver.log.warn(
133
+ `Timed out after ${SessionClaimHandler.RELEASE_WAIT_MS}ms waiting for ` +
134
+ `${util.pluralize('session', pendingSessionIds.length, true)} ` +
135
+ `[${pendingSessionIds.join(', ')}] to release udid '${udid}'. ` +
136
+ `Proceeding with session startup.`,
137
+ );
138
+ }
139
+ } finally {
140
+ contendedSubscription.unsubscribe();
141
+ releasedSubscription.unsubscribe();
142
+ }
143
+ }
144
+
145
+ /** @internal Exposed for unit tests. */
146
+ resetForTesting(): void {
147
+ for (const subscription of this.subscriptionsBySessionId.values()) {
148
+ subscription.unsubscribe();
149
+ }
150
+ this.subscriptionsBySessionId.clear();
151
+ }
152
+
153
+ private async dispatchSessionUdidMessage(
154
+ driver: AndroidUiautomator2Driver,
155
+ udid: string,
156
+ sessionId: string,
157
+ message: IpcMessage<SessionUdidIpcMessage>,
158
+ ): Promise<void> {
159
+ try {
160
+ await this.handleSessionUdidMessage(driver, udid, message);
161
+ } catch (err) {
162
+ const msg = err instanceof Error ? err.message : String(err);
163
+ driver.log.warn(`Could not handle udid claim IPC message for session '${sessionId}': ${msg}`);
164
+ }
165
+ }
166
+
167
+ private async handleSessionUdidMessage(
168
+ driver: AndroidUiautomator2Driver,
169
+ udid: string,
170
+ message: IpcMessage<SessionUdidIpcMessage>,
171
+ ): Promise<void> {
172
+ if (!this.isMatchingSessionUdidMessage(message, udid, driver.sessionId ?? undefined)) {
173
+ return;
174
+ }
175
+
176
+ try {
177
+ await this.publishSessionUdidContended(driver, udid);
178
+ } catch (err) {
179
+ const msg = err instanceof Error ? err.message : String(err);
180
+ driver.log.warn(
181
+ `Could not publish udid contention message for session '${driver.sessionId}': ${msg}`,
182
+ );
183
+ }
184
+
185
+ driver.log.warn(
186
+ `Session '${message.data.sessionId}' is starting on udid '${udid}', which is already in use ` +
187
+ `by another session identified by ${driver.sessionId}. Running multiple parallel sessions on the same ` +
188
+ `device is highly discouraged. Consider enabling the Appium server's '--session-override' flag ` +
189
+ `and make sure to properly quit the previous session before starting a new one. ` +
190
+ `Terminating the obsolete session.`,
191
+ );
192
+ await this.terminateSessionOnRequest(driver, udid);
193
+ }
194
+
195
+ private async publishSessionUdidContended(
196
+ driver: AndroidUiautomator2Driver,
197
+ udid: string,
198
+ ): Promise<void> {
199
+ const ipc = await this.getIpc();
200
+ const sessionId = driver.sessionId ?? undefined;
201
+ if (!ipc || !udid || !sessionId) {
202
+ return;
203
+ }
204
+
205
+ await ipc.publish<SessionUdidIpcMessage>(
206
+ SessionClaimHandler.CONTENDED_TOPIC,
207
+ this.getPublisherId(driver),
208
+ {
209
+ udid,
210
+ sessionId,
211
+ },
212
+ );
213
+ }
214
+
215
+ private async terminateSessionOnRequest(
216
+ driver: AndroidUiautomator2Driver,
217
+ udid: string,
218
+ ): Promise<void> {
219
+ const sessionId = driver.sessionId ?? undefined;
220
+ const publisherId = sessionId ? this.getPublisherId(driver) : undefined;
221
+ const {log} = driver;
222
+
223
+ try {
224
+ await driver.deleteSession();
225
+ } catch (err) {
226
+ const msg = err instanceof Error ? err.message : String(err);
227
+ log.warn(`Could not terminate session '${sessionId}' on IPC request: ${msg}`);
228
+ }
229
+
230
+ await this.publishSessionUdidReleased(log, udid, sessionId, publisherId);
231
+ }
232
+
233
+ private async publishSessionUdidReleased(
234
+ log: AppiumLogger,
235
+ udid: string,
236
+ sessionId: string | undefined,
237
+ publisherId: string | undefined,
238
+ ): Promise<void> {
239
+ try {
240
+ const ipc = await this.getIpc();
241
+ if (!ipc || !udid || !sessionId || !publisherId) {
242
+ return;
243
+ }
244
+
245
+ await ipc.publish<SessionUdidIpcMessage>(SessionClaimHandler.RELEASED_TOPIC, publisherId, {
246
+ udid,
247
+ sessionId,
248
+ });
249
+ } catch (err) {
250
+ const msg = err instanceof Error ? err.message : String(err);
251
+ log.warn(`Could not publish udid release message for session '${sessionId}': ${msg}`);
252
+ }
253
+ }
254
+
255
+ private getPublisherId(driver: AndroidUiautomator2Driver): string {
256
+ return node.getObjectId(driver);
257
+ }
258
+
259
+ private isMatchingSessionUdidMessage(
260
+ message: IpcMessage<SessionUdidIpcMessage>,
261
+ udid: string,
262
+ sessionId: string | undefined,
263
+ ): boolean {
264
+ return message.data.udid === udid && message.data.sessionId !== sessionId;
265
+ }
266
+ }
267
+
268
+ const loadSharedIpc = memoize(async function loadSharedIpc(): Promise<IAppiumIpc | undefined> {
269
+ try {
270
+ const {AppiumIpc} = (await import('appium/driver.js')) as {AppiumIpc?: AppiumIpcConstructor};
271
+ return AppiumIpc ? new AppiumIpc() : undefined;
272
+ } catch {
273
+ return undefined;
274
+ }
275
+ });
276
+
277
+ export const sessionClaimHandler = new SessionClaimHandler(loadSharedIpc);
278
+
279
+ /**
280
+ * @internal Exposed for unit tests.
281
+ */
282
+ export function setSharedIpcForTesting(ipc: IAppiumIpc | undefined): void {
283
+ loadSharedIpc.cache.set(undefined, Promise.resolve(ipc));
284
+ }
285
+
286
+ /**
287
+ * @internal Exposed for unit tests.
288
+ */
289
+ export function resetDriverInstanceIpcForTesting(): void {
290
+ sessionClaimHandler.resetForTesting();
291
+ loadSharedIpc.cache.clear();
292
+ }