appium-mac2-driver 4.3.6 → 4.5.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 +12 -0
- package/WebDriverAgentMac/IntegrationTests/AMSessionTests.m +14 -0
- package/WebDriverAgentMac/IntegrationTests/AMWindowTests.m +427 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/AMWindowCommands.m +63 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBDebugCommands.m +2 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Commands/FBSessionCommands.m +17 -5
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.h +3 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBCommandStatus.m +13 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Routing/FBSession.m +5 -1
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.h +4 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMScreenUtils.m +23 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.h +18 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMSnapshotUtils.m +43 -28
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.h +111 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMWindowUtils.m +780 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.h +61 -0
- package/WebDriverAgentMac/WebDriverAgentLib/Utilities/AMXCTAccessibility.m +108 -0
- package/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +20 -0
- package/build/lib/commands/native-record-screen.d.ts +2 -0
- package/build/lib/commands/native-record-screen.d.ts.map +1 -1
- package/build/lib/commands/native-record-screen.js.map +1 -1
- package/build/lib/commands/windows.d.ts +22 -0
- package/build/lib/commands/windows.d.ts.map +1 -0
- package/build/lib/commands/windows.js +32 -0
- package/build/lib/commands/windows.js.map +1 -0
- package/build/lib/driver.d.ts +15 -0
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +55 -32
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +9 -0
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +9 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/build/lib/session-claim-handler.d.ts +41 -0
- package/build/lib/session-claim-handler.d.ts.map +1 -0
- package/build/lib/session-claim-handler.js +236 -0
- package/build/lib/session-claim-handler.js.map +1 -0
- package/build/lib/types.d.ts +26 -0
- package/build/lib/types.d.ts.map +1 -1
- package/build/lib/wda-mac.d.ts +6 -2
- package/build/lib/wda-mac.d.ts.map +1 -1
- package/build/lib/wda-mac.js +39 -9
- package/build/lib/wda-mac.js.map +1 -1
- package/lib/commands/native-record-screen.ts +2 -0
- package/lib/commands/windows.ts +44 -0
- package/lib/driver.ts +59 -31
- package/lib/execute-method-map.ts +9 -0
- package/lib/session-claim-handler.ts +306 -0
- package/lib/types.ts +28 -0
- package/lib/wda-mac.ts +41 -10
- package/node_modules/appium-xcode/CHANGELOG.md +7 -0
- package/node_modules/appium-xcode/package.json +4 -4
- package/node_modules/async-lock/AUTHORS +21 -0
- package/node_modules/async-lock/History.md +132 -0
- package/node_modules/async-lock/LICENSE +22 -0
- package/node_modules/async-lock/README.md +177 -0
- package/node_modules/async-lock/index.js +2 -0
- package/node_modules/async-lock/lib/index.js +312 -0
- package/node_modules/async-lock/package.json +48 -0
- package/node_modules/bare-fs/binding.c +1 -1
- package/node_modules/bare-fs/index.js +30 -6
- package/node_modules/bare-fs/package.json +1 -1
- package/node_modules/bare-fs/prebuilds/android-arm/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-ia32/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/darwin-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/darwin-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-arm64-simulator/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-x64-simulator/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/linux-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/linux-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/win32-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/win32-x64/bare-fs.bare +0 -0
- package/node_modules/bare-url/README.md +4 -0
- package/node_modules/bare-url/index.d.ts +3 -0
- package/node_modules/bare-url/index.js +42 -18
- package/node_modules/bare-url/lib/errors.js +4 -0
- package/node_modules/bare-url/package.json +1 -1
- package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
- package/node_modules/readable-stream/lib/internal/shims/process.js +19 -0
- package/node_modules/readable-stream/lib/internal/streams/destroy.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/duplexify.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/end-of-stream.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/from.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/legacy.js +6 -1
- package/node_modules/readable-stream/lib/internal/streams/pipeline.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/readable.js +1 -1
- package/node_modules/readable-stream/lib/internal/streams/writable.js +1 -1
- package/node_modules/readable-stream/package.json +3 -2
- package/node_modules/tar-stream/package.json +7 -7
- package/package.json +6 -3
- package/scripts/setup-accessibility.mjs +175 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {Mac2Driver} from '../driver.js';
|
|
2
|
+
import type {AdjustedWindowInfo, WindowInfo, WindowState} from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lists the windows of the current application
|
|
6
|
+
*
|
|
7
|
+
* @returns Information about each window, including its element identifier
|
|
8
|
+
*/
|
|
9
|
+
export async function macosListWindows(this: Mac2Driver): Promise<WindowInfo[]> {
|
|
10
|
+
return (await this.wda.proxy.command('/wda/windows', 'GET')) as WindowInfo[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Changes the state and/or geometry of an application window, optionally moving it to another display
|
|
15
|
+
*
|
|
16
|
+
* @param windowId - Identifier of the window element. The main window is used by default
|
|
17
|
+
* @param state - The state to put the window into
|
|
18
|
+
* @param x - Horizontal origin. Global, or relative to the display if `displayId` is set
|
|
19
|
+
* @param y - Vertical origin. Global, or relative to the display if `displayId` is set
|
|
20
|
+
* @param width - Window width
|
|
21
|
+
* @param height - Window height
|
|
22
|
+
* @param displayId - Identifier of the display to move the window to
|
|
23
|
+
* @returns The updated window information. `windowId` is null if a minimized window was adjusted
|
|
24
|
+
*/
|
|
25
|
+
export async function macosAdjustWindow(
|
|
26
|
+
this: Mac2Driver,
|
|
27
|
+
windowId?: string,
|
|
28
|
+
state?: WindowState,
|
|
29
|
+
x?: number,
|
|
30
|
+
y?: number,
|
|
31
|
+
width?: number,
|
|
32
|
+
height?: number,
|
|
33
|
+
displayId?: number,
|
|
34
|
+
): Promise<AdjustedWindowInfo> {
|
|
35
|
+
return (await this.wda.proxy.command('/wda/window/adjust', 'POST', {
|
|
36
|
+
windowId,
|
|
37
|
+
state,
|
|
38
|
+
x,
|
|
39
|
+
y,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
displayId,
|
|
43
|
+
})) as AdjustedWindowInfo;
|
|
44
|
+
}
|
package/lib/driver.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
DriverOpts,
|
|
12
12
|
W3CDriverCaps,
|
|
13
13
|
} from '@appium/types';
|
|
14
|
-
import {BaseDriver, DeviceSettings} from 'appium/driver.js';
|
|
14
|
+
import {BaseDriver, DeviceSettings, errors} from 'appium/driver.js';
|
|
15
15
|
|
|
16
16
|
import * as appManagemenetCommands from './commands/app-management.js';
|
|
17
17
|
import * as appleScriptCommands from './commands/applescript.js';
|
|
@@ -25,10 +25,12 @@ import * as navigationCommands from './commands/navigation.js';
|
|
|
25
25
|
import * as recordScreenCommands from './commands/record-screen.js';
|
|
26
26
|
import * as screenshotCommands from './commands/screenshots.js';
|
|
27
27
|
import * as sourceCommands from './commands/source.js';
|
|
28
|
+
import * as windowCommands from './commands/windows.js';
|
|
28
29
|
import MAC2_CONSTRAINTS, {type Mac2Constraints} from './constraints.js';
|
|
29
30
|
import {executeMethodMap} from './execute-method-map.js';
|
|
30
31
|
import log from './logger.js';
|
|
31
32
|
import {newMethodMap} from './method-map.js';
|
|
33
|
+
import {sessionClaimHandler} from './session-claim-handler.js';
|
|
32
34
|
import {WDA_MAC_SERVER, type WDAMacServer} from './wda-mac.js';
|
|
33
35
|
|
|
34
36
|
const NO_PROXY: RouteMatcher[] = [
|
|
@@ -110,12 +112,17 @@ export class Mac2Driver
|
|
|
110
112
|
|
|
111
113
|
macosSource = sourceCommands.macosSource;
|
|
112
114
|
|
|
115
|
+
macosListWindows = windowCommands.macosListWindows;
|
|
116
|
+
macosAdjustWindow = windowCommands.macosAdjustWindow;
|
|
117
|
+
|
|
113
118
|
_videoChunksBroadcaster!: nativeScreenRecordingCommands.NativeVideoChunksBroadcaster;
|
|
114
119
|
_screenRecorder: recordScreenCommands.ScreenRecorder | null = null;
|
|
115
120
|
public proxyReqRes!: (...args: any) => any;
|
|
116
121
|
|
|
117
122
|
private isProxyActive: boolean = false;
|
|
118
123
|
private _wda: WDAMacServer | null = null;
|
|
124
|
+
private _wdaSessionId: string | null = null;
|
|
125
|
+
private _hasClaimedHost: boolean = false;
|
|
119
126
|
|
|
120
127
|
constructor(opts: InitialOpts = {} as InitialOpts) {
|
|
121
128
|
super(opts);
|
|
@@ -143,6 +150,13 @@ export class Mac2Driver
|
|
|
143
150
|
if (!this._wda) {
|
|
144
151
|
throw new Error('WDA server is not initialized');
|
|
145
152
|
}
|
|
153
|
+
// The WDA server is shared and only supports a single session, so a newer
|
|
154
|
+
// session replaces this one. Never send commands on its behalf after that.
|
|
155
|
+
if (this._wdaSessionId && !this._wda.isSessionActive(this._wdaSessionId)) {
|
|
156
|
+
throw new errors.NoSuchDriverError(
|
|
157
|
+
'This session has been replaced by a newer one, because this driver only supports a single session',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
146
160
|
return this._wda;
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -150,7 +164,7 @@ export class Mac2Driver
|
|
|
150
164
|
if (!this._wda) {
|
|
151
165
|
return;
|
|
152
166
|
}
|
|
153
|
-
return await this.
|
|
167
|
+
return await this.wda.proxy.command('/appium/settings', 'POST', {
|
|
154
168
|
settings: {[key]: value},
|
|
155
169
|
});
|
|
156
170
|
}
|
|
@@ -170,10 +184,7 @@ export class Mac2Driver
|
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
async proxyCommand(url: string, method: HTTPMethod, body: HTTPBody = null): Promise<any> {
|
|
173
|
-
|
|
174
|
-
throw new Error('WDA server is not initialized');
|
|
175
|
-
}
|
|
176
|
-
return await this._wda.proxy.command(url, method, body);
|
|
187
|
+
return await this.wda.proxy.command(url, method, body);
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
override async getStatus(): Promise<any> {
|
|
@@ -185,10 +196,7 @@ export class Mac2Driver
|
|
|
185
196
|
|
|
186
197
|
// needed to make image plugin work
|
|
187
198
|
async getWindowRect(): Promise<any> {
|
|
188
|
-
|
|
189
|
-
throw new Error('WDA server is not initialized');
|
|
190
|
-
}
|
|
191
|
-
return await this._wda.proxy.command('/window/rect', 'GET');
|
|
199
|
+
return await this.wda.proxy.command('/window/rect', 'GET');
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
override async createSession(
|
|
@@ -202,34 +210,51 @@ export class Mac2Driver
|
|
|
202
210
|
this.caps = caps as Mac2DriverCaps;
|
|
203
211
|
// oxlint-disable-next-line no-self-assign -- narrows this.opts' type for the rest of the method
|
|
204
212
|
this.opts = this.opts as Mac2DriverOpts;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
// The host supports a single session: the previous one has to quit first
|
|
214
|
+
await sessionClaimHandler.runExclusive(async () => {
|
|
215
|
+
// The session might have been deleted while it was waiting for its turn
|
|
216
|
+
if (!this._wda || !this.sessionId) {
|
|
217
|
+
throw new errors.NoSuchDriverError('The session has been deleted before it could start');
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await sessionClaimHandler.registerActiveSession(this);
|
|
221
|
+
await sessionClaimHandler.claimHost(this);
|
|
222
|
+
this._hasClaimedHost = true;
|
|
223
|
+
const prerun = caps.prerun as PrerunCapability | undefined;
|
|
224
|
+
if (prerun) {
|
|
225
|
+
if (typeof prerun.command !== 'string' && typeof prerun.script !== 'string') {
|
|
226
|
+
throw new Error(`'prerun' capability value must either contain 'script' or 'command' entry of string type`);
|
|
227
|
+
}
|
|
228
|
+
log.info('Executing prerun AppleScript');
|
|
229
|
+
const output = await this.macosExecAppleScript(prerun.script, undefined, prerun.command);
|
|
230
|
+
if (output.trim()) {
|
|
231
|
+
log.info(`Prerun script output: ${output}`);
|
|
232
|
+
}
|
|
215
233
|
}
|
|
234
|
+
this._wdaSessionId = await this.wda.startSession(caps, {
|
|
235
|
+
reqBasePath: this.basePath,
|
|
236
|
+
});
|
|
237
|
+
} catch (e: any) {
|
|
238
|
+
await this.deleteSession();
|
|
239
|
+
throw e;
|
|
216
240
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
} catch (e: any) {
|
|
221
|
-
await this.deleteSession();
|
|
222
|
-
throw e;
|
|
223
|
-
}
|
|
224
|
-
this.proxyReqRes = this.wda.proxy.proxyReqRes.bind(this._wda.proxy);
|
|
241
|
+
});
|
|
242
|
+
this.proxyReqRes = (...args: any[]) =>
|
|
243
|
+
(this.wda.proxy.proxyReqRes as (...a: any[]) => any).apply(this.wda.proxy, args);
|
|
225
244
|
this.isProxyActive = true;
|
|
226
245
|
return [sessionId, caps];
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
override async deleteSession(): Promise<void> {
|
|
249
|
+
sessionClaimHandler.unregisterActiveSession(this);
|
|
250
|
+
// A new session waits for the teardown to complete before it starts
|
|
251
|
+
await sessionClaimHandler.trackTeardown(async () => await this.teardownSession());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private async teardownSession(): Promise<void> {
|
|
230
255
|
await this._screenRecorder?.stop(true);
|
|
231
256
|
if (this._videoChunksBroadcaster.hasPublishers) {
|
|
232
|
-
if (this._wda) {
|
|
257
|
+
if (this._wda?.isSessionActive(this._wdaSessionId)) {
|
|
233
258
|
try {
|
|
234
259
|
await this.wda.proxy.command('/wda/video/stop', 'POST', {});
|
|
235
260
|
} catch {}
|
|
@@ -237,10 +262,11 @@ export class Mac2Driver
|
|
|
237
262
|
await this._videoChunksBroadcaster.shutdown(5000);
|
|
238
263
|
}
|
|
239
264
|
if (this._wda) {
|
|
240
|
-
await this.
|
|
265
|
+
await this._wda.stopSession(this._wdaSessionId);
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
|
|
268
|
+
// Skip postrun if the host was never acquired, as the previous session might still be using it
|
|
269
|
+
const postrun = this._hasClaimedHost ? (this.opts.postrun as PostrunCapability | undefined) : undefined;
|
|
244
270
|
if (postrun) {
|
|
245
271
|
if (typeof postrun.command !== 'string' && typeof postrun.script !== 'string') {
|
|
246
272
|
log.error(`'postrun' capability value must either contain 'script' or 'command' entry of string type`);
|
|
@@ -264,6 +290,8 @@ export class Mac2Driver
|
|
|
264
290
|
|
|
265
291
|
private resetState(): void {
|
|
266
292
|
this._wda = null;
|
|
293
|
+
this._wdaSessionId = null;
|
|
294
|
+
this._hasClaimedHost = false;
|
|
267
295
|
this.isProxyActive = false;
|
|
268
296
|
this._videoChunksBroadcaster = new nativeScreenRecordingCommands.NativeVideoChunksBroadcaster(
|
|
269
297
|
this.eventEmitter,
|
|
@@ -130,6 +130,15 @@ export const executeMethodMap = {
|
|
|
130
130
|
optional: ['displayId'],
|
|
131
131
|
},
|
|
132
132
|
},
|
|
133
|
+
'macos: listWindows': {
|
|
134
|
+
command: 'macosListWindows',
|
|
135
|
+
},
|
|
136
|
+
'macos: adjustWindow': {
|
|
137
|
+
command: 'macosAdjustWindow',
|
|
138
|
+
params: {
|
|
139
|
+
optional: ['windowId', 'state', 'x', 'y', 'width', 'height', 'displayId'],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
133
142
|
'macos: appleScript': {
|
|
134
143
|
command: 'macosExecAppleScript',
|
|
135
144
|
params: {
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {setTimeout as delay} from 'node:timers/promises';
|
|
2
|
+
|
|
3
|
+
import type {AppiumLogger, IAppiumIpc, IIpcSubscription, IpcMessage} from '@appium/types';
|
|
4
|
+
import {errors} from 'appium/driver.js';
|
|
5
|
+
import {node, util} from 'appium/support.js';
|
|
6
|
+
import AsyncLock from 'async-lock';
|
|
7
|
+
import {waitForCondition} from 'asyncbox';
|
|
8
|
+
|
|
9
|
+
import type {Mac2Driver} from './driver.js';
|
|
10
|
+
|
|
11
|
+
export type SessionIpcMessage = {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type AppiumIpcConstructor = new () => IAppiumIpc;
|
|
16
|
+
type IpcProvider = () => Promise<IAppiumIpc | undefined>;
|
|
17
|
+
|
|
18
|
+
/** Lets a new session make the previous one quit, over a driver-wide IPC (per-session IPCs are isolated). */
|
|
19
|
+
export class SessionClaimHandler {
|
|
20
|
+
static readonly CLAIMED_TOPIC = 'mac2:sessionClaimed';
|
|
21
|
+
static readonly CONTENDED_TOPIC = 'mac2:sessionContended';
|
|
22
|
+
static readonly RELEASED_TOPIC = 'mac2:sessionReleased';
|
|
23
|
+
|
|
24
|
+
private static readonly STARTUP_LOCK_KEY = 'mac2:sessionStartup';
|
|
25
|
+
|
|
26
|
+
private static readonly CONTENTION_PROBE_MS = 10;
|
|
27
|
+
private static readonly DEFAULT_RELEASE_WAIT_MS = 15000;
|
|
28
|
+
|
|
29
|
+
private readonly startupLock = new AsyncLock();
|
|
30
|
+
private readonly pendingTeardowns = new Set<Promise<void>>();
|
|
31
|
+
private readonly subscriptionsBySessionId = new Map<string, IIpcSubscription<SessionIpcMessage>>();
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly getIpc: IpcProvider,
|
|
35
|
+
private readonly releaseWaitMs: number = SessionClaimHandler.DEFAULT_RELEASE_WAIT_MS,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/** Serializes session startups so the newest wins. Enter before registering the session. */
|
|
39
|
+
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
|
|
40
|
+
return await this.startupLock.acquire(SessionClaimHandler.STARTUP_LOCK_KEY, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Subscribe the session to claim messages from newer sessions. */
|
|
44
|
+
async registerActiveSession(driver: Mac2Driver): Promise<void> {
|
|
45
|
+
const ipc = await this.getIpc();
|
|
46
|
+
const sessionId = driver.sessionId;
|
|
47
|
+
if (!ipc || !sessionId) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.unregisterActiveSession(driver);
|
|
52
|
+
|
|
53
|
+
const subscription = ipc.subscribe<SessionIpcMessage>(
|
|
54
|
+
SessionClaimHandler.CLAIMED_TOPIC,
|
|
55
|
+
this.getPublisherId(driver),
|
|
56
|
+
);
|
|
57
|
+
subscription.on('message', (message) => {
|
|
58
|
+
void this.dispatchClaimMessage(driver, sessionId, message);
|
|
59
|
+
});
|
|
60
|
+
this.subscriptionsBySessionId.set(sessionId, subscription);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Unsubscribe the session from claim messages. */
|
|
64
|
+
unregisterActiveSession(driver: Mac2Driver): void {
|
|
65
|
+
const sessionId = driver.sessionId;
|
|
66
|
+
if (!sessionId) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.subscriptionsBySessionId.get(sessionId)?.unsubscribe();
|
|
71
|
+
this.subscriptionsBySessionId.delete(sessionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Tracks a session teardown, so that a new session does not start before it is complete. */
|
|
75
|
+
async trackTeardown(teardown: () => Promise<void>): Promise<void> {
|
|
76
|
+
const promise = teardown();
|
|
77
|
+
const tracked = (async () => {
|
|
78
|
+
try {
|
|
79
|
+
await promise;
|
|
80
|
+
} catch {
|
|
81
|
+
// the failure is reported to the caller of trackTeardown()
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
this.pendingTeardowns.add(tracked);
|
|
85
|
+
try {
|
|
86
|
+
await promise;
|
|
87
|
+
} finally {
|
|
88
|
+
this.pendingTeardowns.delete(tracked);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Make any other active session quit and wait until it has released the host. */
|
|
93
|
+
async claimHost(driver: Mac2Driver): Promise<void> {
|
|
94
|
+
await this.requestRelease(driver);
|
|
95
|
+
await this.waitForPendingTeardowns(driver);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async waitForPendingTeardowns(driver: Mac2Driver): Promise<void> {
|
|
99
|
+
if (this.pendingTeardowns.size === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
driver.log.debug(`Waiting for ${util.pluralize('session', this.pendingTeardowns.size, true)} to finish quitting`);
|
|
104
|
+
const timer = new AbortController();
|
|
105
|
+
const timedOut = Symbol('timedOut');
|
|
106
|
+
const timeout = (async () => {
|
|
107
|
+
try {
|
|
108
|
+
return await delay(this.releaseWaitMs, timedOut, {signal: timer.signal});
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined; // aborted
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
try {
|
|
114
|
+
if ((await Promise.race([Promise.all(this.pendingTeardowns), timeout])) === timedOut) {
|
|
115
|
+
// Starting now could let the old teardown (e.g. postrun) interfere with this session
|
|
116
|
+
throw new errors.SessionNotCreatedError(
|
|
117
|
+
`The previous session did not finish quitting within ${this.releaseWaitMs}ms`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
timer.abort();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async requestRelease(driver: Mac2Driver): Promise<void> {
|
|
126
|
+
const ipc = await this.getIpc();
|
|
127
|
+
if (!ipc) {
|
|
128
|
+
driver.log.debug('Driver-instance IPC is unavailable. Skipping the session claim.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sessionId = driver.sessionId;
|
|
133
|
+
if (!sessionId) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const contendingSessionIds = new Set<string>();
|
|
138
|
+
const releasedSessionIds = new Set<string>();
|
|
139
|
+
const subscriptions: IIpcSubscription<SessionIpcMessage>[] = [];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const contendedSubscription = ipc.subscribe<SessionIpcMessage>(
|
|
143
|
+
SessionClaimHandler.CONTENDED_TOPIC,
|
|
144
|
+
this.getPublisherId(driver),
|
|
145
|
+
);
|
|
146
|
+
subscriptions.push(contendedSubscription);
|
|
147
|
+
const releasedSubscription = ipc.subscribe<SessionIpcMessage>(
|
|
148
|
+
SessionClaimHandler.RELEASED_TOPIC,
|
|
149
|
+
this.getPublisherId(driver),
|
|
150
|
+
);
|
|
151
|
+
subscriptions.push(releasedSubscription);
|
|
152
|
+
contendedSubscription.on('message', (message) => {
|
|
153
|
+
if (message.data.sessionId !== sessionId) {
|
|
154
|
+
contendingSessionIds.add(message.data.sessionId);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
releasedSubscription.on('message', (message) => {
|
|
158
|
+
if (message.data.sessionId !== sessionId) {
|
|
159
|
+
releasedSessionIds.add(message.data.sessionId);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await ipc.publish<SessionIpcMessage>(SessionClaimHandler.CLAIMED_TOPIC, this.getPublisherId(driver), {
|
|
164
|
+
sessionId,
|
|
165
|
+
});
|
|
166
|
+
// Older sessions reply synchronously to the claim, so a short wait is enough to learn about them
|
|
167
|
+
await delay(SessionClaimHandler.CONTENTION_PROBE_MS);
|
|
168
|
+
|
|
169
|
+
if (contendingSessionIds.size === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await waitForCondition(() => [...contendingSessionIds].every((id) => releasedSessionIds.has(id)), {
|
|
175
|
+
waitMs: this.releaseWaitMs,
|
|
176
|
+
intervalMs: 50,
|
|
177
|
+
});
|
|
178
|
+
driver.log.debug(
|
|
179
|
+
`Received release confirmation from ${util.pluralize('session', contendingSessionIds.size, true)}`,
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
const pendingSessionIds = [...contendingSessionIds].filter((id) => !releasedSessionIds.has(id));
|
|
183
|
+
// Starting now could let the old session interfere with this one
|
|
184
|
+
throw new errors.SessionNotCreatedError(
|
|
185
|
+
`${util.pluralize('session', pendingSessionIds.length, true)} [${pendingSessionIds.join(', ')}] ` +
|
|
186
|
+
`did not quit within ${this.releaseWaitMs}ms`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
subscriptions.forEach((sub) => sub.unsubscribe());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @internal Exposed for unit tests. */
|
|
195
|
+
resetForTesting(): void {
|
|
196
|
+
this.pendingTeardowns.clear();
|
|
197
|
+
for (const subscription of this.subscriptionsBySessionId.values()) {
|
|
198
|
+
subscription.unsubscribe();
|
|
199
|
+
}
|
|
200
|
+
this.subscriptionsBySessionId.clear();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async dispatchClaimMessage(
|
|
204
|
+
driver: Mac2Driver,
|
|
205
|
+
sessionId: string,
|
|
206
|
+
message: IpcMessage<SessionIpcMessage>,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
await this.handleClaimMessage(driver, sessionId, message);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
212
|
+
driver.log.warn(`Could not handle the session claim IPC message for session '${sessionId}': ${msg}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async handleClaimMessage(
|
|
217
|
+
driver: Mac2Driver,
|
|
218
|
+
sessionId: string,
|
|
219
|
+
message: IpcMessage<SessionIpcMessage>,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
if (message.data.sessionId === sessionId || driver.sessionId !== sessionId) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Tell the claimer that it has to wait for us
|
|
226
|
+
await this.publish(driver, SessionClaimHandler.CONTENDED_TOPIC, sessionId);
|
|
227
|
+
|
|
228
|
+
driver.log.warn(
|
|
229
|
+
`Session '${message.data.sessionId}' is starting, while this session ('${sessionId}') is still active. ` +
|
|
230
|
+
`Only one session at a time is supported, since XCTest is single-threaded. ` +
|
|
231
|
+
`Consider enabling the Appium server's '--session-override' flag and make sure to properly ` +
|
|
232
|
+
`quit the previous session before starting a new one. Terminating the obsolete session.`,
|
|
233
|
+
);
|
|
234
|
+
await this.terminateSession(driver, sessionId);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async terminateSession(driver: Mac2Driver, sessionId: string): Promise<void> {
|
|
238
|
+
const publisherId = this.getPublisherId(driver);
|
|
239
|
+
const {log} = driver;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Unlike deleteSession(), this also makes the server drop the session from its list
|
|
243
|
+
await driver.startUnexpectedShutdown(
|
|
244
|
+
new errors.NoSuchDriverError(
|
|
245
|
+
'This session has been replaced by a newer one, because only a single session is supported',
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
log.warn(`Could not terminate session '${sessionId}' on IPC request: ${msg}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await this.publish(driver, SessionClaimHandler.RELEASED_TOPIC, sessionId, publisherId, log);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async publish(
|
|
257
|
+
driver: Mac2Driver,
|
|
258
|
+
topic: string,
|
|
259
|
+
sessionId: string,
|
|
260
|
+
publisherId: string = this.getPublisherId(driver),
|
|
261
|
+
log: AppiumLogger = driver.log,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
try {
|
|
264
|
+
const ipc = await this.getIpc();
|
|
265
|
+
await ipc?.publish<SessionIpcMessage>(topic, publisherId, {sessionId});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
268
|
+
log.warn(`Could not publish '${topic}' message for session '${sessionId}': ${msg}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private getPublisherId(driver: Mac2Driver): string {
|
|
273
|
+
return node.getObjectId(driver);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let sharedIpc: Promise<IAppiumIpc | undefined> | undefined;
|
|
278
|
+
|
|
279
|
+
async function loadSharedIpc(): Promise<IAppiumIpc | undefined> {
|
|
280
|
+
sharedIpc ??= (async () => {
|
|
281
|
+
try {
|
|
282
|
+
const {AppiumIpc} = (await import('appium/driver.js')) as {AppiumIpc?: AppiumIpcConstructor};
|
|
283
|
+
return AppiumIpc ? new AppiumIpc() : undefined;
|
|
284
|
+
} catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
return await sharedIpc;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const sessionClaimHandler = new SessionClaimHandler(loadSharedIpc);
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @internal Exposed for unit tests.
|
|
295
|
+
*/
|
|
296
|
+
export function setSharedIpcForTesting(ipc: IAppiumIpc | undefined): void {
|
|
297
|
+
sharedIpc = Promise.resolve(ipc);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @internal Exposed for unit tests.
|
|
302
|
+
*/
|
|
303
|
+
export function resetSharedIpcForTesting(): void {
|
|
304
|
+
sessionClaimHandler.resetForTesting();
|
|
305
|
+
sharedIpc = undefined;
|
|
306
|
+
}
|
package/lib/types.ts
CHANGED
|
@@ -26,3 +26,31 @@ export interface ScreenshotInfo {
|
|
|
26
26
|
|
|
27
27
|
/** A dictionary where each key contains a unique display identifier */
|
|
28
28
|
export type ScreenshotsInfo = StringRecord<ScreenshotInfo>;
|
|
29
|
+
|
|
30
|
+
export interface Rect {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type WindowState = 'normal' | 'minimized' | 'maximized' | 'fullscreen';
|
|
38
|
+
|
|
39
|
+
export interface WindowInfo {
|
|
40
|
+
/** Identifier of the window element */
|
|
41
|
+
windowId: string;
|
|
42
|
+
title: string;
|
|
43
|
+
rect: Rect;
|
|
44
|
+
/** The display that contains most of the window, if any */
|
|
45
|
+
displayId: number | null;
|
|
46
|
+
/** Traits below are null if the Accessibility permission is not granted */
|
|
47
|
+
isMain: boolean | null;
|
|
48
|
+
isFocused: boolean | null;
|
|
49
|
+
isMinimized: boolean | null;
|
|
50
|
+
isFullscreen: boolean | null;
|
|
51
|
+
isMovable: boolean | null;
|
|
52
|
+
isResizable: boolean | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `windowId` is null if a minimized window has been adjusted */
|
|
56
|
+
export type AdjustedWindowInfo = Omit<WindowInfo, 'windowId'> & {windowId: string | null};
|
package/lib/wda-mac.ts
CHANGED
|
@@ -301,7 +301,8 @@ export class WDAMacServer {
|
|
|
301
301
|
return this._proxy;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
/** Starts a WDA session, replacing the previous one. Returns the new WDA session id. */
|
|
305
|
+
async startSession(caps: StartSessionCapabilities, opts: SessionOptions = {}): Promise<string> {
|
|
305
306
|
this._serverStartupTimeoutMs = caps.serverStartupTimeout ?? this._serverStartupTimeoutMs;
|
|
306
307
|
|
|
307
308
|
this._isProxyingToRemoteServer = !!caps.webDriverAgentMacUrl;
|
|
@@ -381,23 +382,53 @@ export class WDAMacServer {
|
|
|
381
382
|
log.info('The host process has already been listening. Proceeding with session creation');
|
|
382
383
|
}
|
|
383
384
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
385
|
+
// WDA drops the previous session as soon as it gets this request,
|
|
386
|
+
// so a failure must not leave the proxy pointing at the replaced one
|
|
387
|
+
try {
|
|
388
|
+
await this._proxy.command('/session', 'POST', {
|
|
389
|
+
capabilities: {
|
|
390
|
+
firstMatch: [{}],
|
|
391
|
+
alwaysMatch: caps,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
} catch (e) {
|
|
395
|
+
this._proxy.sessionId = null;
|
|
396
|
+
throw e;
|
|
397
|
+
}
|
|
398
|
+
const sessionId = this._proxy.sessionId;
|
|
399
|
+
if (!sessionId) {
|
|
400
|
+
throw new errors.SessionNotCreatedError('WebDriverAgentMac did not return a session id');
|
|
401
|
+
}
|
|
402
|
+
return sessionId;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Whether the WDA session has not been replaced by a newer one. */
|
|
406
|
+
isSessionActive(sessionId: string | null | undefined): boolean {
|
|
407
|
+
return !!sessionId && this._proxy?.sessionId === sessionId;
|
|
390
408
|
}
|
|
391
409
|
|
|
392
|
-
|
|
410
|
+
/** Stops the WDA session. A replaced one is skipped, so the newer one is never affected. */
|
|
411
|
+
async stopSession(sessionId: string | null | undefined): Promise<void> {
|
|
393
412
|
if (!this._isProxyingToRemoteServer && !this._process?.isRunning) {
|
|
394
413
|
log.info(`Mac2Driver session cannot be stopped, because the server is not running`);
|
|
395
414
|
return;
|
|
396
415
|
}
|
|
397
416
|
|
|
398
|
-
if (this.
|
|
417
|
+
if (!this.isSessionActive(sessionId)) {
|
|
418
|
+
if (sessionId) {
|
|
419
|
+
log.info(`Mac2Driver session '${sessionId}' is not active anymore. Nothing to stop`);
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const proxy = this._proxy;
|
|
425
|
+
if (proxy) {
|
|
399
426
|
try {
|
|
400
|
-
await
|
|
427
|
+
await proxy.command(`/session/${sessionId}`, 'DELETE');
|
|
428
|
+
// A newer session might have been created while the DELETE was pending
|
|
429
|
+
if (proxy.sessionId === sessionId) {
|
|
430
|
+
proxy.sessionId = null;
|
|
431
|
+
}
|
|
401
432
|
} catch (e: any) {
|
|
402
433
|
log.info(`Mac2Driver session cannot be deleted. Original error: ${e.message}`);
|
|
403
434
|
}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [7.2.3](https://github.com/appium/appium-ios/compare/appium-xcode@7.2.2...appium-xcode@7.2.3) (2026-10-02)
|
|
7
|
+
|
|
8
|
+
### Miscellaneous Chores
|
|
9
|
+
|
|
10
|
+
* ensure Dependabot preserves version ranges ([#24](https://github.com/appium/appium-ios/issues/24)) ([95e4e1a](https://github.com/appium/appium-ios/commit/95e4e1a4703f8ca833f01ecf2af75f4696d98043))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
## [7.2.2](https://github.com/appium/appium-ios/compare/appium-xcode@7.2.1...appium-xcode@7.2.2) (2026-10-01)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package appium-xcode
|