appium-webdriveragent 12.1.1 → 12.2.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 +6 -0
- package/Scripts/build-webdriveragent.mjs +9 -2
- package/Scripts/fetch-prebuilt-wda.mjs +12 -6
- package/WebDriverAgentLib/Info.plist +2 -2
- package/build/lib/check-dependencies.d.ts +3 -0
- package/build/lib/check-dependencies.d.ts.map +1 -1
- package/build/lib/check-dependencies.js +15 -12
- package/build/lib/check-dependencies.js.map +1 -1
- package/build/lib/utils.d.ts +22 -8
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +73 -21
- package/build/lib/utils.js.map +1 -1
- package/build/lib/webdriveragent.d.ts +42 -39
- package/build/lib/webdriveragent.d.ts.map +1 -1
- package/build/lib/webdriveragent.js +119 -106
- package/build/lib/webdriveragent.js.map +1 -1
- package/build/lib/xcodebuild.d.ts +5 -5
- package/build/lib/xcodebuild.d.ts.map +1 -1
- package/build/lib/xcodebuild.js +12 -13
- package/build/lib/xcodebuild.js.map +1 -1
- package/lib/check-dependencies.ts +18 -15
- package/lib/utils.ts +97 -40
- package/lib/webdriveragent.ts +132 -117
- package/lib/xcodebuild.ts +17 -15
- package/package.json +2 -6
|
@@ -5,21 +5,9 @@ import {WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP} from './constants';
|
|
|
5
5
|
import {BOOTSTRAP_PATH} from './utils';
|
|
6
6
|
import type {XcodeBuild} from './xcodebuild';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
|
|
12
|
-
'-scheme',
|
|
13
|
-
WDA_SCHEME,
|
|
14
|
-
'-sdk',
|
|
15
|
-
SDK_SIMULATOR,
|
|
16
|
-
'CODE_SIGN_IDENTITY=""',
|
|
17
|
-
'CODE_SIGNING_REQUIRED="NO"',
|
|
18
|
-
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
|
|
19
|
-
];
|
|
20
|
-
await exec('xcodebuild', args);
|
|
21
|
-
}
|
|
22
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Ensure simulator WDA is built and return the resulting app bundle path.
|
|
10
|
+
*/
|
|
23
11
|
export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
|
|
24
12
|
const derivedDataPath = await xcodebuild.retrieveDerivedDataPath();
|
|
25
13
|
if (!derivedDataPath) {
|
|
@@ -38,3 +26,18 @@ export async function bundleWDASim(xcodebuild: XcodeBuild): Promise<string> {
|
|
|
38
26
|
await buildWDASim();
|
|
39
27
|
return wdaBundlePath;
|
|
40
28
|
}
|
|
29
|
+
|
|
30
|
+
async function buildWDASim(): Promise<void> {
|
|
31
|
+
const args = [
|
|
32
|
+
'-project',
|
|
33
|
+
path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'),
|
|
34
|
+
'-scheme',
|
|
35
|
+
WDA_SCHEME,
|
|
36
|
+
'-sdk',
|
|
37
|
+
SDK_SIMULATOR,
|
|
38
|
+
'CODE_SIGN_IDENTITY=""',
|
|
39
|
+
'CODE_SIGNING_REQUIRED="NO"',
|
|
40
|
+
'GCC_TREAT_WARNINGS_AS_ERRORS=0',
|
|
41
|
+
];
|
|
42
|
+
await exec('xcodebuild', args);
|
|
43
|
+
}
|
package/lib/utils.ts
CHANGED
|
@@ -3,9 +3,7 @@ import {exec, SubProcess} from 'teen_process';
|
|
|
3
3
|
import path, {dirname} from 'node:path';
|
|
4
4
|
import {fileURLToPath} from 'node:url';
|
|
5
5
|
import {log} from './logger';
|
|
6
|
-
import _ from 'lodash';
|
|
7
6
|
import {PLATFORM_NAME_TVOS} from './constants';
|
|
8
|
-
import B from 'bluebird';
|
|
9
7
|
import _fs from 'node:fs';
|
|
10
8
|
import {waitForCondition} from 'asyncbox';
|
|
11
9
|
import {arch} from 'node:os';
|
|
@@ -19,13 +17,18 @@ const currentFilename =
|
|
|
19
17
|
|
|
20
18
|
const currentDirname = dirname(currentFilename);
|
|
21
19
|
|
|
20
|
+
let moduleRootCache: string | undefined;
|
|
21
|
+
|
|
22
22
|
/**
|
|
23
23
|
* Calculates the path to the current module's root folder
|
|
24
24
|
*
|
|
25
25
|
* @returns {string} The full path to module root
|
|
26
26
|
* @throws {Error} If the current module root folder cannot be determined
|
|
27
27
|
*/
|
|
28
|
-
const getModuleRoot =
|
|
28
|
+
const getModuleRoot = function getModuleRoot(): string {
|
|
29
|
+
if (moduleRootCache) {
|
|
30
|
+
return moduleRootCache;
|
|
31
|
+
}
|
|
29
32
|
let currentDir = currentDirname;
|
|
30
33
|
let isAtFsRoot = false;
|
|
31
34
|
while (!isAtFsRoot) {
|
|
@@ -35,6 +38,7 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string {
|
|
|
35
38
|
_fs.existsSync(manifestPath) &&
|
|
36
39
|
JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent'
|
|
37
40
|
) {
|
|
41
|
+
moduleRootCache = currentDir;
|
|
38
42
|
return currentDir;
|
|
39
43
|
}
|
|
40
44
|
} catch {}
|
|
@@ -42,15 +46,29 @@ const getModuleRoot = _.memoize(function getModuleRoot(): string {
|
|
|
42
46
|
isAtFsRoot = currentDir.length <= path.dirname(currentDir).length;
|
|
43
47
|
}
|
|
44
48
|
throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module');
|
|
45
|
-
}
|
|
49
|
+
};
|
|
46
50
|
|
|
47
51
|
export const BOOTSTRAP_PATH = getModuleRoot();
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Arguments for setting xctestrun file
|
|
55
|
+
*/
|
|
56
|
+
export interface XctestrunFileArgs {
|
|
57
|
+
deviceInfo: DeviceInfo;
|
|
58
|
+
sdkVersion: string;
|
|
59
|
+
bootstrapPath: string;
|
|
60
|
+
wdaRemotePort: number | string;
|
|
61
|
+
wdaBindingIP?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find and terminate all processes matching the given pgrep pattern.
|
|
66
|
+
*/
|
|
49
67
|
export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
|
|
50
68
|
const signals = [2, 15, 9];
|
|
51
69
|
for (const signal of signals) {
|
|
52
70
|
const matchedPids = await getPIDsUsingPattern(pgrepPattern);
|
|
53
|
-
if (
|
|
71
|
+
if (matchedPids.length === 0) {
|
|
54
72
|
return;
|
|
55
73
|
}
|
|
56
74
|
const args = [`-${signal}`, ...matchedPids];
|
|
@@ -59,21 +77,24 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
|
|
|
59
77
|
} catch (err: any) {
|
|
60
78
|
log.debug(`kill ${args.join(' ')} -> ${err.message}`);
|
|
61
79
|
}
|
|
62
|
-
if (signal ===
|
|
80
|
+
if (signal === signals[signals.length - 1]) {
|
|
63
81
|
// there is no need to wait after SIGKILL
|
|
64
82
|
return;
|
|
65
83
|
}
|
|
66
84
|
try {
|
|
67
85
|
await waitForCondition(
|
|
68
86
|
async () => {
|
|
69
|
-
const pidCheckPromises = matchedPids.map((pid) =>
|
|
70
|
-
|
|
87
|
+
const pidCheckPromises = matchedPids.map(async (pid) => {
|
|
88
|
+
try {
|
|
89
|
+
await exec('kill', ['-0', pid]);
|
|
71
90
|
// the process is still alive
|
|
72
|
-
|
|
91
|
+
return false;
|
|
92
|
+
} catch {
|
|
73
93
|
// the process is dead
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return (await Promise.all(pidCheckPromises)).every((x) => x === true);
|
|
77
98
|
},
|
|
78
99
|
{
|
|
79
100
|
waitMs: 1000,
|
|
@@ -93,9 +114,12 @@ export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
|
|
|
93
114
|
* @returns Return true if the platformName is tvOS
|
|
94
115
|
*/
|
|
95
116
|
export function isTvOS(platformName: string): boolean {
|
|
96
|
-
return
|
|
117
|
+
return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase();
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Configure keychain access required for real-device code signing.
|
|
122
|
+
*/
|
|
99
123
|
export async function setRealDeviceSecurity(
|
|
100
124
|
keychainPath: string,
|
|
101
125
|
keychainPassword: string,
|
|
@@ -106,17 +130,6 @@ export async function setRealDeviceSecurity(
|
|
|
106
130
|
await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]);
|
|
107
131
|
}
|
|
108
132
|
|
|
109
|
-
/**
|
|
110
|
-
* Arguments for setting xctestrun file
|
|
111
|
-
*/
|
|
112
|
-
export interface XctestrunFileArgs {
|
|
113
|
-
deviceInfo: DeviceInfo;
|
|
114
|
-
sdkVersion: string;
|
|
115
|
-
bootstrapPath: string;
|
|
116
|
-
wdaRemotePort: number | string;
|
|
117
|
-
wdaBindingIP?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
133
|
/**
|
|
121
134
|
* Creates xctestrun file per device & platform version.
|
|
122
135
|
* We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
|
|
@@ -140,7 +153,7 @@ export async function setXctestrunFile(args: XctestrunFileArgs): Promise<string>
|
|
|
140
153
|
wdaRemotePort,
|
|
141
154
|
wdaBindingIP,
|
|
142
155
|
);
|
|
143
|
-
const newXctestRunContent =
|
|
156
|
+
const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort);
|
|
144
157
|
await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true);
|
|
145
158
|
|
|
146
159
|
return xctestrunFilePath;
|
|
@@ -285,6 +298,23 @@ export async function getWDAUpgradeTimestamp(): Promise<number | null> {
|
|
|
285
298
|
return mtime.getTime();
|
|
286
299
|
}
|
|
287
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Escape regular expression metacharacters in a string.
|
|
303
|
+
*/
|
|
304
|
+
export function escapeRegExp(value: string): string {
|
|
305
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Truncate a string to the given length and append ellipsis if needed.
|
|
310
|
+
*/
|
|
311
|
+
export function truncateString(value: string, length: number): string {
|
|
312
|
+
if (value.length <= length) {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
return `${value.slice(0, Math.max(0, length - 1))}…`;
|
|
316
|
+
}
|
|
317
|
+
|
|
288
318
|
/**
|
|
289
319
|
* Kills running XCTest processes for the particular device.
|
|
290
320
|
*/
|
|
@@ -296,7 +326,7 @@ export async function resetTestProcesses(udid: string, isSimulator: boolean): Pr
|
|
|
296
326
|
processPatterns.push(`xctest.*${udid}`);
|
|
297
327
|
}
|
|
298
328
|
log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`);
|
|
299
|
-
await
|
|
329
|
+
await Promise.all(processPatterns.map(killAppUsingPattern));
|
|
300
330
|
}
|
|
301
331
|
|
|
302
332
|
/**
|
|
@@ -329,22 +359,25 @@ export async function getPIDsListeningOnPort(
|
|
|
329
359
|
return result;
|
|
330
360
|
}
|
|
331
361
|
|
|
332
|
-
if (
|
|
362
|
+
if (typeof filteringFunc !== 'function') {
|
|
333
363
|
return result;
|
|
334
364
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
365
|
+
const filtered = await Promise.all(
|
|
366
|
+
result.map(async (pid) => {
|
|
367
|
+
let stdout: string;
|
|
368
|
+
try {
|
|
369
|
+
({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
|
|
370
|
+
} catch (e: any) {
|
|
371
|
+
if (e.code === 1) {
|
|
372
|
+
// The process does not exist anymore, there's nothing to filter
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
throw e;
|
|
343
376
|
}
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
377
|
+
return (await filteringFunc(stdout)) ? pid : null;
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
return filtered.filter((pid): pid is string => Boolean(pid));
|
|
348
381
|
}
|
|
349
382
|
|
|
350
383
|
// Private functions
|
|
@@ -359,7 +392,7 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
|
|
|
359
392
|
return stdout
|
|
360
393
|
.split(/\s+/)
|
|
361
394
|
.map((x) => parseInt(x, 10))
|
|
362
|
-
.filter(
|
|
395
|
+
.filter(Number.isInteger)
|
|
363
396
|
.map((x) => `${x}`);
|
|
364
397
|
} catch (err: any) {
|
|
365
398
|
log.debug(
|
|
@@ -368,3 +401,27 @@ async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
|
|
|
368
401
|
return [];
|
|
369
402
|
}
|
|
370
403
|
}
|
|
404
|
+
|
|
405
|
+
function mergeObjects<T extends Record<string, any>, U extends Record<string, any>>(
|
|
406
|
+
target: T,
|
|
407
|
+
source: U,
|
|
408
|
+
): T & U {
|
|
409
|
+
const output: Record<string, any> = {...target};
|
|
410
|
+
for (const [key, sourceValue] of Object.entries(source)) {
|
|
411
|
+
const targetValue = output[key];
|
|
412
|
+
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
413
|
+
output[key] = mergeObjects(targetValue, sourceValue);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
output[key] = sourceValue;
|
|
417
|
+
}
|
|
418
|
+
return output as T & U;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isPlainObject(value: unknown): value is Record<string, any> {
|
|
422
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
const prototype = Object.getPrototypeOf(value);
|
|
426
|
+
return prototype === Object.prototype || prototype === null;
|
|
427
|
+
}
|
package/lib/webdriveragent.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import {waitForCondition} from 'asyncbox';
|
|
2
|
-
import _ from 'lodash';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import url from 'node:url';
|
|
5
|
-
import B from 'bluebird';
|
|
6
3
|
import {JWProxy} from '@appium/base-driver';
|
|
7
4
|
import {fs, util, plist} from '@appium/support';
|
|
8
5
|
import type {AppiumLogger, StringRecord} from '@appium/types';
|
|
@@ -34,41 +31,42 @@ const WDA_AGENT_PORT = 8100;
|
|
|
34
31
|
const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner';
|
|
35
32
|
const SHARED_RESOURCES_GUARD = new AsyncLock();
|
|
36
33
|
const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion';
|
|
34
|
+
const URL_PROTOCOL_SEPARATOR = '://';
|
|
37
35
|
|
|
38
36
|
export class WebDriverAgent {
|
|
39
37
|
bootstrapPath: string;
|
|
40
38
|
agentPath: string;
|
|
41
39
|
readonly args: WebDriverAgentArgs;
|
|
42
|
-
private readonly log: AppiumLogger;
|
|
43
40
|
readonly device: AppleDevice;
|
|
44
41
|
readonly platformVersion?: string;
|
|
45
42
|
readonly platformName?: string;
|
|
46
43
|
readonly iosSdkVersion?: string;
|
|
47
44
|
readonly host?: string;
|
|
48
45
|
readonly isRealDevice: boolean;
|
|
49
|
-
private readonly wdaBundlePath?: string;
|
|
50
|
-
private readonly wdaLocalPort?: number;
|
|
51
46
|
readonly wdaRemotePort: number;
|
|
52
47
|
readonly wdaBaseUrl: string;
|
|
53
48
|
readonly wdaBindingIP?: string;
|
|
54
|
-
private readonly prebuildWDA?: boolean;
|
|
55
49
|
webDriverAgentUrl?: string;
|
|
56
50
|
started: boolean;
|
|
51
|
+
updatedWDABundleId?: string;
|
|
52
|
+
noSessionProxy?: NoSessionProxy;
|
|
53
|
+
jwproxy?: JWProxy;
|
|
54
|
+
proxyReqRes?: any;
|
|
55
|
+
private readonly log: AppiumLogger;
|
|
56
|
+
private readonly wdaBundlePath?: string;
|
|
57
|
+
private readonly wdaLocalPort?: number;
|
|
58
|
+
private readonly prebuildWDA?: boolean;
|
|
57
59
|
private readonly wdaConnectionTimeout?: number;
|
|
58
60
|
private readonly useXctestrunFile?: boolean;
|
|
59
61
|
private readonly usePrebuiltWDA?: boolean;
|
|
60
62
|
private readonly derivedDataPath?: string;
|
|
61
63
|
private readonly mjpegServerPort?: number;
|
|
62
|
-
updatedWDABundleId?: string;
|
|
63
64
|
private readonly wdaLaunchTimeout: number;
|
|
64
65
|
private readonly usePreinstalledWDA?: boolean;
|
|
65
|
-
private xctestApiClient?: Xctest | null;
|
|
66
66
|
private readonly updatedWDABundleIdSuffix: string;
|
|
67
|
+
private xctestApiClient?: Xctest | null;
|
|
67
68
|
private _xcodebuild?: XcodeBuild | null;
|
|
68
|
-
|
|
69
|
-
jwproxy?: JWProxy;
|
|
70
|
-
proxyReqRes?: any;
|
|
71
|
-
private _url?: url.UrlWithStringQuery;
|
|
69
|
+
private _url?: URL;
|
|
72
70
|
|
|
73
71
|
/**
|
|
74
72
|
* Creates a new WebDriverAgent instance.
|
|
@@ -76,7 +74,7 @@ export class WebDriverAgent {
|
|
|
76
74
|
* @param log - Optional logger instance
|
|
77
75
|
*/
|
|
78
76
|
constructor(args: WebDriverAgentArgs, log: AppiumLogger | null = null) {
|
|
79
|
-
this.args =
|
|
77
|
+
this.args = {...args};
|
|
80
78
|
this.log = log ?? defaultLogger;
|
|
81
79
|
|
|
82
80
|
this.device = args.device;
|
|
@@ -185,6 +183,62 @@ export class WebDriverAgent {
|
|
|
185
183
|
return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`;
|
|
186
184
|
}
|
|
187
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Gets the base path for the WebDriverAgent URL.
|
|
188
|
+
* @returns The base path (empty string if root path)
|
|
189
|
+
*/
|
|
190
|
+
get basePath(): string {
|
|
191
|
+
if (this.url.pathname === '/') {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
return this.url.pathname || '';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets the WebDriverAgent URL.
|
|
199
|
+
* Constructs the URL from webDriverAgentUrl if provided, otherwise
|
|
200
|
+
* builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort.
|
|
201
|
+
* @returns The parsed URL object
|
|
202
|
+
*/
|
|
203
|
+
get url(): URL {
|
|
204
|
+
if (!this._url) {
|
|
205
|
+
if (this.webDriverAgentUrl) {
|
|
206
|
+
this._url = this.toUrl(this.webDriverAgentUrl);
|
|
207
|
+
} else {
|
|
208
|
+
const port = this.wdaLocalPort || WDA_AGENT_PORT;
|
|
209
|
+
const parsedBaseUrl = this.toUrl(this.wdaBaseUrl || WDA_BASE_URL);
|
|
210
|
+
this._url = new URL(
|
|
211
|
+
`${parsedBaseUrl.protocol}//${this.wdaBindingIP || parsedBaseUrl.hostname}:${port}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return this._url;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Gets whether WebDriverAgent has fully started.
|
|
220
|
+
* @returns `true` if WDA has started, `false` otherwise
|
|
221
|
+
*/
|
|
222
|
+
get fullyStarted(): boolean {
|
|
223
|
+
return this.started;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Sets whether WebDriverAgent has fully started.
|
|
228
|
+
* @param started - `true` if WDA has started, `false` otherwise
|
|
229
|
+
*/
|
|
230
|
+
set fullyStarted(started: boolean) {
|
|
231
|
+
this.started = started ?? false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Sets the WebDriverAgent URL.
|
|
236
|
+
* @param _url - The URL string to parse and set
|
|
237
|
+
*/
|
|
238
|
+
set url(_url: string) {
|
|
239
|
+
this._url = this.toUrl(_url);
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
/**
|
|
189
243
|
* Cleans up obsolete cached processes from previous WDA sessions
|
|
190
244
|
* that are listening on the same port but belong to different devices.
|
|
@@ -197,7 +251,7 @@ export class WebDriverAgent {
|
|
|
197
251
|
!cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()),
|
|
198
252
|
);
|
|
199
253
|
|
|
200
|
-
if (
|
|
254
|
+
if (obsoletePids.length === 0) {
|
|
201
255
|
this.log.debug(
|
|
202
256
|
`No obsolete cached processes from previous WDA sessions ` +
|
|
203
257
|
`listening on port ${this.url.port} have been found`,
|
|
@@ -220,14 +274,6 @@ export class WebDriverAgent {
|
|
|
220
274
|
}
|
|
221
275
|
|
|
222
276
|
/**
|
|
223
|
-
* Gets the base path for the WebDriverAgent URL.
|
|
224
|
-
* @returns The base path (empty string if root path)
|
|
225
|
-
*/
|
|
226
|
-
get basePath(): string {
|
|
227
|
-
if (this.url.path === '/') {
|
|
228
|
-
return '';
|
|
229
|
-
}
|
|
230
|
-
return this.url.path || '';
|
|
231
277
|
}
|
|
232
278
|
|
|
233
279
|
/**
|
|
@@ -307,56 +353,10 @@ export class WebDriverAgent {
|
|
|
307
353
|
* @returns `true` if source is fresh (all required files exist), `false` otherwise
|
|
308
354
|
*/
|
|
309
355
|
async isSourceFresh(): Promise<boolean> {
|
|
310
|
-
const existsPromises = ['Resources',
|
|
356
|
+
const existsPromises = ['Resources', path.join('Resources', 'WebDriverAgent.bundle')].map(
|
|
311
357
|
(subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)),
|
|
312
358
|
);
|
|
313
|
-
return (await
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private async parseBundleId(wdaBundlePath: string): Promise<string> {
|
|
317
|
-
const infoPlistPath = path.join(wdaBundlePath, 'Info.plist');
|
|
318
|
-
const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as {
|
|
319
|
-
CFBundleIdentifier?: string;
|
|
320
|
-
};
|
|
321
|
-
if (!infoPlist.CFBundleIdentifier) {
|
|
322
|
-
throw new Error(`Could not find bundle id in '${infoPlistPath}'`);
|
|
323
|
-
}
|
|
324
|
-
return infoPlist.CFBundleIdentifier;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private async fetchWDABundle(): Promise<string> {
|
|
328
|
-
if (!this.derivedDataPath) {
|
|
329
|
-
return await bundleWDASim(this.xcodebuild);
|
|
330
|
-
}
|
|
331
|
-
const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, {
|
|
332
|
-
absolute: true,
|
|
333
|
-
});
|
|
334
|
-
if (_.isEmpty(wdaBundlePaths)) {
|
|
335
|
-
throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`);
|
|
336
|
-
}
|
|
337
|
-
return wdaBundlePaths[0];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private setupProxies(sessionId: string): void {
|
|
341
|
-
const proxyOpts: any = {
|
|
342
|
-
log: this.log,
|
|
343
|
-
server: this.url.hostname ?? undefined,
|
|
344
|
-
port: parseInt(this.url.port ?? '', 10) || undefined,
|
|
345
|
-
base: this.basePath,
|
|
346
|
-
timeout: this.wdaConnectionTimeout,
|
|
347
|
-
keepAlive: true,
|
|
348
|
-
scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http',
|
|
349
|
-
headers: this.args.extraRequestHeaders,
|
|
350
|
-
};
|
|
351
|
-
if (this.args.reqBasePath) {
|
|
352
|
-
proxyOpts.reqBasePath = this.args.reqBasePath;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
this.jwproxy = new JWProxy(proxyOpts);
|
|
356
|
-
this.jwproxy.sessionId = sessionId;
|
|
357
|
-
this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);
|
|
358
|
-
|
|
359
|
-
this.noSessionProxy = new NoSessionProxy(proxyOpts);
|
|
359
|
+
return (await Promise.all(existsPromises)).every((v) => v === true);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/**
|
|
@@ -401,49 +401,6 @@ export class WebDriverAgent {
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
/**
|
|
405
|
-
* Gets the WebDriverAgent URL.
|
|
406
|
-
* Constructs the URL from webDriverAgentUrl if provided, otherwise
|
|
407
|
-
* builds it from wdaBaseUrl, wdaBindingIP, and wdaLocalPort.
|
|
408
|
-
* @returns The parsed URL object
|
|
409
|
-
*/
|
|
410
|
-
get url(): url.UrlWithStringQuery {
|
|
411
|
-
if (!this._url) {
|
|
412
|
-
if (this.webDriverAgentUrl) {
|
|
413
|
-
this._url = url.parse(this.webDriverAgentUrl);
|
|
414
|
-
} else {
|
|
415
|
-
const port = this.wdaLocalPort || WDA_AGENT_PORT;
|
|
416
|
-
const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL);
|
|
417
|
-
this._url = url.parse(`${protocol}//${this.wdaBindingIP || hostname}:${port}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return this._url;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Sets the WebDriverAgent URL.
|
|
425
|
-
* @param _url - The URL string to parse and set
|
|
426
|
-
*/
|
|
427
|
-
set url(_url: string) {
|
|
428
|
-
this._url = url.parse(_url);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Gets whether WebDriverAgent has fully started.
|
|
433
|
-
* @returns `true` if WDA has started, `false` otherwise
|
|
434
|
-
*/
|
|
435
|
-
get fullyStarted(): boolean {
|
|
436
|
-
return this.started;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Sets whether WebDriverAgent has fully started.
|
|
441
|
-
* @param started - `true` if WDA has started, `false` otherwise
|
|
442
|
-
*/
|
|
443
|
-
set fullyStarted(started: boolean) {
|
|
444
|
-
this.started = started ?? false;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
404
|
/**
|
|
448
405
|
* Retrieves the Xcode derived data path for WebDriverAgent.
|
|
449
406
|
* @returns The derived data path, or `undefined` if xcodebuild is skipped
|
|
@@ -497,7 +454,7 @@ export class WebDriverAgent {
|
|
|
497
454
|
if (
|
|
498
455
|
actualUpgradeTimestamp &&
|
|
499
456
|
upgradedAt &&
|
|
500
|
-
|
|
457
|
+
`${actualUpgradeTimestamp}`.toLowerCase() !== `${upgradedAt}`.toLowerCase()
|
|
501
458
|
) {
|
|
502
459
|
this.log.info(
|
|
503
460
|
'Will uninstall running WDA since it has different version in comparison to the one ' +
|
|
@@ -523,6 +480,64 @@ export class WebDriverAgent {
|
|
|
523
480
|
await this.uninstall();
|
|
524
481
|
}
|
|
525
482
|
|
|
483
|
+
private async parseBundleId(wdaBundlePath: string): Promise<string> {
|
|
484
|
+
const infoPlistPath = path.join(wdaBundlePath, 'Info.plist');
|
|
485
|
+
const infoPlist = (await plist.parsePlist(await fs.readFile(infoPlistPath))) as {
|
|
486
|
+
CFBundleIdentifier?: string;
|
|
487
|
+
};
|
|
488
|
+
if (!infoPlist.CFBundleIdentifier) {
|
|
489
|
+
throw new Error(`Could not find bundle id in '${infoPlistPath}'`);
|
|
490
|
+
}
|
|
491
|
+
return infoPlist.CFBundleIdentifier;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private async fetchWDABundle(): Promise<string> {
|
|
495
|
+
if (!this.derivedDataPath) {
|
|
496
|
+
return await bundleWDASim(this.xcodebuild);
|
|
497
|
+
}
|
|
498
|
+
const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, {
|
|
499
|
+
absolute: true,
|
|
500
|
+
});
|
|
501
|
+
if (wdaBundlePaths.length === 0) {
|
|
502
|
+
throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`);
|
|
503
|
+
}
|
|
504
|
+
return wdaBundlePaths[0];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private setupProxies(sessionId: string): void {
|
|
508
|
+
const proxyOpts: any = {
|
|
509
|
+
log: this.log,
|
|
510
|
+
server: this.url.hostname ?? undefined,
|
|
511
|
+
port: parseInt(this.url.port ?? '', 10) || undefined,
|
|
512
|
+
base: this.basePath,
|
|
513
|
+
timeout: this.wdaConnectionTimeout,
|
|
514
|
+
keepAlive: true,
|
|
515
|
+
scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http',
|
|
516
|
+
headers: this.args.extraRequestHeaders,
|
|
517
|
+
};
|
|
518
|
+
if (this.args.reqBasePath) {
|
|
519
|
+
proxyOpts.reqBasePath = this.args.reqBasePath;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.jwproxy = new JWProxy(proxyOpts);
|
|
523
|
+
this.jwproxy.sessionId = sessionId;
|
|
524
|
+
this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);
|
|
525
|
+
|
|
526
|
+
this.noSessionProxy = new NoSessionProxy(proxyOpts);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private toUrl(value: string): URL {
|
|
530
|
+
// Treat values without `://` as host/path inputs and normalize to http.
|
|
531
|
+
if (!value.includes(URL_PROTOCOL_SEPARATOR)) {
|
|
532
|
+
return new URL(`http://${value}`);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
return new URL(value);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error(`Invalid URL: ${value}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
526
541
|
private setWDAPaths(bootstrapPath?: string, agentPath?: string): void {
|
|
527
542
|
// allow the user to specify a place for WDA. This is undocumented and
|
|
528
543
|
// only here for the purposes of testing development of WDA
|
|
@@ -572,7 +587,7 @@ export class WebDriverAgent {
|
|
|
572
587
|
const sendGetStatus = async () =>
|
|
573
588
|
(await noSessionProxy.command('/status', 'GET')) as StringRecord;
|
|
574
589
|
|
|
575
|
-
if (
|
|
590
|
+
if (timeoutMs == null || timeoutMs <= 0) {
|
|
576
591
|
try {
|
|
577
592
|
return await sendGetStatus();
|
|
578
593
|
} catch (err: any) {
|
|
@@ -619,7 +634,7 @@ export class WebDriverAgent {
|
|
|
619
634
|
private async uninstall(): Promise<void> {
|
|
620
635
|
try {
|
|
621
636
|
const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME);
|
|
622
|
-
if (
|
|
637
|
+
if (bundleIds.length === 0) {
|
|
623
638
|
this.log.debug('No WDAs on the device.');
|
|
624
639
|
return;
|
|
625
640
|
}
|