appium-uiautomator2-driver 6.1.0 → 6.1.2
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/build/lib/constraints.d.ts +1 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +1 -5
- package/build/lib/driver.js.map +1 -1
- package/build/lib/uiautomator2.d.ts +49 -83
- package/build/lib/uiautomator2.d.ts.map +1 -1
- package/build/lib/uiautomator2.js +150 -190
- package/build/lib/uiautomator2.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/lib/driver.ts +4 -9
- package/lib/{uiautomator2.js → uiautomator2.ts} +291 -261
- package/npm-shrinkwrap.json +130 -161
- package/package.json +2 -2
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {JWProxy, errors} from 'appium/driver';
|
|
3
|
+
import {waitForCondition} from 'asyncbox';
|
|
4
4
|
import {
|
|
5
5
|
SERVER_APK_PATH as apkPath,
|
|
6
6
|
TEST_APK_PATH as testApkPath,
|
|
7
|
-
version as serverVersion
|
|
7
|
+
version as serverVersion,
|
|
8
8
|
} from 'appium-uiautomator2-server';
|
|
9
|
-
import {
|
|
9
|
+
import {util, timing} from 'appium/support';
|
|
10
|
+
import type {
|
|
11
|
+
AppiumLogger,
|
|
12
|
+
StringRecord,
|
|
13
|
+
HTTPMethod,
|
|
14
|
+
HTTPBody,
|
|
15
|
+
ProxyResponse,
|
|
16
|
+
ProxyOptions,
|
|
17
|
+
} from '@appium/types';
|
|
10
18
|
import B from 'bluebird';
|
|
11
19
|
import axios from 'axios';
|
|
20
|
+
import type {ADB, InstallState} from 'appium-adb';
|
|
21
|
+
import type {SubProcess} from 'teen_process';
|
|
12
22
|
|
|
13
|
-
const REQD_PARAMS = ['adb', 'tmpDir', 'host', 'systemPort', 'devicePort', 'disableWindowAnimation'];
|
|
14
23
|
const SERVER_LAUNCH_TIMEOUT_MS = 30000;
|
|
15
24
|
const SERVER_INSTALL_RETRIES = 20;
|
|
16
25
|
const SERVICES_LAUNCH_TIMEOUT_MS = 30000;
|
|
@@ -19,59 +28,57 @@ const SERVER_REQUEST_TIMEOUT_MS = 500;
|
|
|
19
28
|
export const SERVER_PACKAGE_ID = 'io.appium.uiautomator2.server';
|
|
20
29
|
export const SERVER_TEST_PACKAGE_ID = `${SERVER_PACKAGE_ID}.test`;
|
|
21
30
|
export const INSTRUMENTATION_TARGET = `${SERVER_TEST_PACKAGE_ID}/androidx.test.runner.AndroidJUnitRunner`;
|
|
31
|
+
const REQUIRED_OPTIONS: RequiredKeysOf<UiAutomator2ServerOptions>[] = [
|
|
32
|
+
'adb',
|
|
33
|
+
'host',
|
|
34
|
+
'systemPort',
|
|
35
|
+
'disableWindowAnimation',
|
|
36
|
+
] as const;
|
|
22
37
|
|
|
23
38
|
class UIA2Proxy extends JWProxy {
|
|
24
|
-
|
|
25
|
-
didInstrumentationExit;
|
|
39
|
+
public didInstrumentationExit: boolean = false;
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
override async proxyCommand(
|
|
42
|
+
url: string,
|
|
43
|
+
method: HTTPMethod,
|
|
44
|
+
body: HTTPBody = null,
|
|
45
|
+
): Promise<[ProxyResponse, HTTPBody]> {
|
|
31
46
|
if (this.didInstrumentationExit) {
|
|
32
47
|
throw new errors.InvalidContextError(
|
|
33
48
|
`'${method} ${url}' cannot be proxied to UiAutomator2 server because ` +
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
'the instrumentation process is not running (probably crashed). ' +
|
|
50
|
+
'Check the server log and/or the logcat output for more details',
|
|
51
|
+
);
|
|
36
52
|
}
|
|
37
53
|
return await super.proxyCommand(url, method, body);
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
export class UiAutomator2Server {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {import('@appium/types').AppiumLogger} log
|
|
63
|
-
* @param {UiAutomator2ServerOptions} opts
|
|
64
|
-
*/
|
|
65
|
-
constructor (log, opts) {
|
|
66
|
-
for (const req of REQD_PARAMS) {
|
|
67
|
-
if (!opts || !util.hasValue(opts[req])) {
|
|
68
|
-
throw new Error(`Option '${req}' is required!`);
|
|
58
|
+
public readonly jwproxy: UIA2Proxy;
|
|
59
|
+
public readonly proxyReqRes: typeof UIA2Proxy.prototype.proxyReqRes;
|
|
60
|
+
public readonly proxyCommand: typeof UIA2Proxy.prototype.command;
|
|
61
|
+
|
|
62
|
+
private readonly host: string;
|
|
63
|
+
private readonly systemPort: number;
|
|
64
|
+
private readonly adb: ADB;
|
|
65
|
+
private readonly disableWindowAnimation: boolean;
|
|
66
|
+
private readonly disableSuppressAccessibilityService?: boolean;
|
|
67
|
+
private readonly log: AppiumLogger;
|
|
68
|
+
private instrumentationProcess: SubProcess | null = null;
|
|
69
|
+
|
|
70
|
+
constructor(log: AppiumLogger, opts: UiAutomator2ServerOptions) {
|
|
71
|
+
// Validate and assign required properties from UiAutomator2ServerOptions
|
|
72
|
+
// The keys are typed to match only the required (non-optional) properties of the interface
|
|
73
|
+
for (const key of REQUIRED_OPTIONS) {
|
|
74
|
+
if (!opts || !util.hasValue(opts[key])) {
|
|
75
|
+
throw new Error(`Option '${key}' is required!`);
|
|
69
76
|
}
|
|
70
|
-
this[
|
|
77
|
+
(this as any)[key] = opts[key];
|
|
71
78
|
}
|
|
72
79
|
this.log = log;
|
|
73
80
|
this.disableSuppressAccessibilityService = opts.disableSuppressAccessibilityService;
|
|
74
|
-
const proxyOpts = {
|
|
81
|
+
const proxyOpts: ProxyOptions = {
|
|
75
82
|
log,
|
|
76
83
|
server: this.host,
|
|
77
84
|
port: this.systemPort,
|
|
@@ -90,90 +97,23 @@ export class UiAutomator2Server {
|
|
|
90
97
|
this.instrumentationProcess = null;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
/**
|
|
94
|
-
* @param {string} appPath
|
|
95
|
-
* @param {string} appId
|
|
96
|
-
* @returns {Promise<{installState: import('appium-adb').InstallState, appPath: string; appId: string}>}
|
|
97
|
-
*/
|
|
98
|
-
async prepareServerPackage(appPath, appId) {
|
|
99
|
-
const resultInfo = {
|
|
100
|
-
installState: this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
|
|
101
|
-
appPath,
|
|
102
|
-
appId,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (appId === SERVER_TEST_PACKAGE_ID && await this.adb.isAppInstalled(appId)) {
|
|
106
|
-
// There is no point in getting the state for the test server,
|
|
107
|
-
// since it does not contain any version info
|
|
108
|
-
resultInfo.installState = this.adb.APP_INSTALL_STATE.SAME_VERSION_INSTALLED;
|
|
109
|
-
} else if (appId === SERVER_PACKAGE_ID) {
|
|
110
|
-
resultInfo.installState = await this.adb.getApplicationInstallState(resultInfo.appPath, appId);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return resultInfo;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @typedef {Object} PackageInfo
|
|
118
|
-
* @property {import('appium-adb').InstallState} installState
|
|
119
|
-
* @property {string} appPath
|
|
120
|
-
* @property {string} appId
|
|
121
|
-
*/
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Checks if server components must be installed from the device under test
|
|
125
|
-
* in scope of the current driver session.
|
|
126
|
-
*
|
|
127
|
-
* For example, if one of servers on the device under test was newer than servers current UIA2 driver wants to
|
|
128
|
-
* use for the session, the UIA2 driver should uninstall the installed ones in order to avoid
|
|
129
|
-
* version mismatch between the UIA2 drier and servers on the device under test.
|
|
130
|
-
* Also, if the device under test has missing servers, current UIA2 driver should uninstall all
|
|
131
|
-
* servers once in order to install proper servers freshly.
|
|
132
|
-
*
|
|
133
|
-
* @param {PackageInfo[]} packagesInfo
|
|
134
|
-
* @returns {boolean} true if any of components is already installed and the other is not installed
|
|
135
|
-
* or the installed one has a newer version.
|
|
136
|
-
*/
|
|
137
|
-
shouldUninstallServerPackages(packagesInfo = []) {
|
|
138
|
-
const isAnyComponentInstalled = packagesInfo.some(
|
|
139
|
-
({installState}) => installState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED);
|
|
140
|
-
const isAnyComponentNotInstalledOrNewer = packagesInfo.some(({installState}) => [
|
|
141
|
-
this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
|
|
142
|
-
this.adb.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED,
|
|
143
|
-
].includes(installState));
|
|
144
|
-
return isAnyComponentInstalled && isAnyComponentNotInstalledOrNewer;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Checks if server components should be installed on the device under test in scope of the current driver session.
|
|
149
|
-
*
|
|
150
|
-
* @param {PackageInfo[]} packagesInfo
|
|
151
|
-
* @returns {boolean} true if any of components is not installed or older than currently installed in order to
|
|
152
|
-
* install or upgrade the servers on the device under test.
|
|
153
|
-
*/
|
|
154
|
-
shouldInstallServerPackages(packagesInfo = []) {
|
|
155
|
-
return packagesInfo.some(({installState}) => [
|
|
156
|
-
this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
|
|
157
|
-
this.adb.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED,
|
|
158
|
-
].includes(installState));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
100
|
/**
|
|
162
101
|
* Installs the apks on to the device or emulator.
|
|
163
102
|
*
|
|
164
|
-
* @param
|
|
103
|
+
* @param installTimeout - Installation timeout
|
|
165
104
|
*/
|
|
166
|
-
async installServerApk
|
|
105
|
+
async installServerApk(installTimeout: number = SERVER_INSTALL_RETRIES * 1000): Promise<void> {
|
|
167
106
|
const packagesInfo = await B.all(
|
|
168
107
|
[
|
|
169
108
|
{
|
|
170
109
|
appPath: apkPath,
|
|
171
110
|
appId: SERVER_PACKAGE_ID,
|
|
172
|
-
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
173
113
|
appPath: testApkPath,
|
|
174
114
|
appId: SERVER_TEST_PACKAGE_ID,
|
|
175
115
|
},
|
|
176
|
-
].map(({appPath, appId}) => this.prepareServerPackage(appPath, appId))
|
|
116
|
+
].map(({appPath, appId}) => this.prepareServerPackage(appPath, appId)),
|
|
177
117
|
);
|
|
178
118
|
|
|
179
119
|
this.log.debug(`Server packages status: ${JSON.stringify(packagesInfo)}`);
|
|
@@ -186,22 +126,22 @@ export class UiAutomator2Server {
|
|
|
186
126
|
this.log.info('Full packages reinstall is going to be performed');
|
|
187
127
|
}
|
|
188
128
|
if (shouldUninstallServerPackages) {
|
|
189
|
-
const silentUninstallPkg = async (pkgId) => {
|
|
129
|
+
const silentUninstallPkg = async (pkgId: string): Promise<void> => {
|
|
190
130
|
try {
|
|
191
131
|
await this.adb.uninstallApk(pkgId);
|
|
192
|
-
} catch (err) {
|
|
132
|
+
} catch (err: any) {
|
|
193
133
|
this.log.info(`Cannot uninstall '${pkgId}': ${err.message}`);
|
|
194
134
|
}
|
|
195
135
|
};
|
|
196
136
|
await B.all(packagesInfo.map(({appId}) => silentUninstallPkg(appId)));
|
|
197
137
|
}
|
|
198
138
|
if (shouldInstallServerPackages) {
|
|
199
|
-
const installPkg = async (pkgPath) => {
|
|
139
|
+
const installPkg = async (pkgPath: string): Promise<void> => {
|
|
200
140
|
await this.adb.install(pkgPath, {
|
|
201
141
|
noIncremental: true,
|
|
202
142
|
replace: true,
|
|
203
143
|
timeout: installTimeout,
|
|
204
|
-
timeoutCapName: 'uiautomator2ServerInstallTimeout'
|
|
144
|
+
timeoutCapName: 'uiautomator2ServerInstallTimeout',
|
|
205
145
|
});
|
|
206
146
|
};
|
|
207
147
|
await B.all(packagesInfo.map(({appPath}) => installPkg(appPath)));
|
|
@@ -210,50 +150,7 @@ export class UiAutomator2Server {
|
|
|
210
150
|
await this.verifyServicesAvailability();
|
|
211
151
|
}
|
|
212
152
|
|
|
213
|
-
async
|
|
214
|
-
this.log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT_MS}ms for services to be available`);
|
|
215
|
-
let isPmServiceAvailable = false;
|
|
216
|
-
let pmOutput = '';
|
|
217
|
-
let pmError = null;
|
|
218
|
-
try {
|
|
219
|
-
await waitForCondition(async () => {
|
|
220
|
-
if (!isPmServiceAvailable) {
|
|
221
|
-
pmError = null;
|
|
222
|
-
pmOutput = '';
|
|
223
|
-
try {
|
|
224
|
-
pmOutput = await this.adb.shell(['pm', 'list', 'instrumentation']);
|
|
225
|
-
} catch (e) {
|
|
226
|
-
pmError = e;
|
|
227
|
-
}
|
|
228
|
-
if (pmOutput.includes('Could not access the Package Manager')) {
|
|
229
|
-
pmError = new Error(`Problem running Package Manager: ${pmOutput}`);
|
|
230
|
-
pmOutput = ''; // remove output, so it is not printed below
|
|
231
|
-
} else if (pmOutput.includes(INSTRUMENTATION_TARGET)) {
|
|
232
|
-
pmOutput = ''; // remove output, so it is not printed below
|
|
233
|
-
this.log.debug(`Instrumentation target '${INSTRUMENTATION_TARGET}' is available`);
|
|
234
|
-
isPmServiceAvailable = true;
|
|
235
|
-
} else if (!pmError) {
|
|
236
|
-
pmError = new Error('The instrumentation target is not listed by Package Manager');
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return isPmServiceAvailable;
|
|
240
|
-
}, {
|
|
241
|
-
waitMs: SERVICES_LAUNCH_TIMEOUT_MS,
|
|
242
|
-
intervalMs: 1000,
|
|
243
|
-
});
|
|
244
|
-
} catch {
|
|
245
|
-
// @ts-ignore It is ok if the attribute does not exist
|
|
246
|
-
this.log.error(`Unable to find instrumentation target '${INSTRUMENTATION_TARGET}': ${(pmError || {}).message}`);
|
|
247
|
-
if (pmOutput) {
|
|
248
|
-
this.log.debug('Available targets:');
|
|
249
|
-
for (const line of pmOutput.split('\n')) {
|
|
250
|
-
this.log.debug(` ${line.replace('instrumentation:', '')}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async startSession (caps) {
|
|
153
|
+
async startSession(caps: StringRecord): Promise<void> {
|
|
257
154
|
await this.cleanupAutomationLeftovers();
|
|
258
155
|
if (caps.skipServerInstallation) {
|
|
259
156
|
this.log.info(`'skipServerInstallation' is set. Attempting to use UIAutomator2 server from the device`);
|
|
@@ -262,7 +159,7 @@ export class UiAutomator2Server {
|
|
|
262
159
|
this.log.info(`Using UIAutomator2 server from '${apkPath}' and test from '${testApkPath}'`);
|
|
263
160
|
}
|
|
264
161
|
|
|
265
|
-
const timeout = caps.uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT_MS;
|
|
162
|
+
const timeout = (caps.uiautomator2ServerLaunchTimeout as number) || SERVER_LAUNCH_TIMEOUT_MS;
|
|
266
163
|
const timer = new timing.Timer().start();
|
|
267
164
|
let retries = 0;
|
|
268
165
|
const maxRetries = 2;
|
|
@@ -276,23 +173,26 @@ export class UiAutomator2Server {
|
|
|
276
173
|
await this.startInstrumentationProcess();
|
|
277
174
|
if (!this.jwproxy.didInstrumentationExit) {
|
|
278
175
|
try {
|
|
279
|
-
await waitForCondition(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
176
|
+
await waitForCondition(
|
|
177
|
+
async () => {
|
|
178
|
+
try {
|
|
179
|
+
await this.jwproxy.command('/status', 'GET');
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
// short circuit to retry or fail fast
|
|
183
|
+
return this.jwproxy.didInstrumentationExit;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
waitMs: timeout,
|
|
188
|
+
intervalMs: 1000,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
291
191
|
} catch {
|
|
292
192
|
throw this.log.errorWithException(
|
|
293
|
-
`The instrumentation process cannot be initialized within ${timeout}ms timeout. `
|
|
294
|
-
|
|
295
|
-
|
|
193
|
+
`The instrumentation process cannot be initialized within ${timeout}ms timeout. ` +
|
|
194
|
+
'Make sure the application under test does not crash and investigate the logcat output. ' +
|
|
195
|
+
`You could also try to increase the value of 'uiautomator2ServerLaunchTimeout' capability`,
|
|
296
196
|
);
|
|
297
197
|
}
|
|
298
198
|
}
|
|
@@ -303,27 +203,166 @@ export class UiAutomator2Server {
|
|
|
303
203
|
retries++;
|
|
304
204
|
if (retries >= maxRetries) {
|
|
305
205
|
throw this.log.errorWithException(
|
|
306
|
-
'The instrumentation process cannot be initialized. '
|
|
307
|
-
|
|
206
|
+
'The instrumentation process cannot be initialized. ' +
|
|
207
|
+
'Make sure the application under test does not crash and investigate the logcat output.',
|
|
308
208
|
);
|
|
309
209
|
}
|
|
310
|
-
this.log.warn(
|
|
311
|
-
|
|
210
|
+
this.log.warn(
|
|
211
|
+
`The instrumentation process has been unexpectedly terminated. ` +
|
|
212
|
+
`Retrying UiAutomator2 startup (#${retries} of ${maxRetries - 1})`,
|
|
213
|
+
);
|
|
312
214
|
await this.cleanupAutomationLeftovers(true);
|
|
313
215
|
await B.delay(delayBetweenRetries);
|
|
314
216
|
}
|
|
315
217
|
|
|
316
|
-
this.log.debug(
|
|
317
|
-
|
|
218
|
+
this.log.debug(
|
|
219
|
+
`The initialization of the instrumentation process took ` +
|
|
220
|
+
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
|
|
221
|
+
);
|
|
318
222
|
await this.jwproxy.command('/session', 'POST', {
|
|
319
223
|
capabilities: {
|
|
320
224
|
firstMatch: [caps],
|
|
321
225
|
alwaysMatch: {},
|
|
322
|
-
}
|
|
226
|
+
},
|
|
323
227
|
});
|
|
324
228
|
}
|
|
325
229
|
|
|
326
|
-
async
|
|
230
|
+
async deleteSession(): Promise<void> {
|
|
231
|
+
this.log.debug('Deleting UiAutomator2 server session');
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await this.jwproxy.command('/', 'DELETE');
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
this.log.warn(
|
|
237
|
+
`Did not get the confirmation of UiAutomator2 server session deletion. ` +
|
|
238
|
+
`Original error: ${err.message}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Theoretically we could also force kill instumentation and server processes
|
|
243
|
+
// without waiting for them to properly quit on their own.
|
|
244
|
+
// This may cause unexpected error reports in device logs though.
|
|
245
|
+
await this._waitForTermination();
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await this.stopInstrumentationProcess();
|
|
249
|
+
} catch (err: any) {
|
|
250
|
+
this.log.warn(`Could not stop the instrumentation process. Original error: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await B.all([this.adb.forceStop(SERVER_PACKAGE_ID), this.adb.forceStop(SERVER_TEST_PACKAGE_ID)]);
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async prepareServerPackage(appPath: string, appId: string): Promise<PackageInfo> {
|
|
259
|
+
const resultInfo: PackageInfo = {
|
|
260
|
+
installState: this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
|
|
261
|
+
appPath,
|
|
262
|
+
appId,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (appId === SERVER_TEST_PACKAGE_ID && (await this.adb.isAppInstalled(appId))) {
|
|
266
|
+
// There is no point in getting the state for the test server,
|
|
267
|
+
// since it does not contain any version info
|
|
268
|
+
resultInfo.installState = this.adb.APP_INSTALL_STATE.SAME_VERSION_INSTALLED;
|
|
269
|
+
} else if (appId === SERVER_PACKAGE_ID) {
|
|
270
|
+
resultInfo.installState = await this.adb.getApplicationInstallState(resultInfo.appPath, appId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return resultInfo;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Checks if server components must be installed from the device under test
|
|
278
|
+
* in scope of the current driver session.
|
|
279
|
+
*
|
|
280
|
+
* For example, if one of servers on the device under test was newer than servers current UIA2 driver wants to
|
|
281
|
+
* use for the session, the UIA2 driver should uninstall the installed ones in order to avoid
|
|
282
|
+
* version mismatch between the UIA2 drier and servers on the device under test.
|
|
283
|
+
* Also, if the device under test has missing servers, current UIA2 driver should uninstall all
|
|
284
|
+
* servers once in order to install proper servers freshly.
|
|
285
|
+
*
|
|
286
|
+
* @param packagesInfo
|
|
287
|
+
* @returns true if any of components is already installed and the other is not installed
|
|
288
|
+
* or the installed one has a newer version.
|
|
289
|
+
*/
|
|
290
|
+
private shouldUninstallServerPackages(packagesInfo: PackageInfo[] = []): boolean {
|
|
291
|
+
const isAnyComponentInstalled = packagesInfo.some(
|
|
292
|
+
({installState}) => installState !== this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
|
|
293
|
+
);
|
|
294
|
+
const isAnyComponentNotInstalledOrNewer = packagesInfo.some(({installState}) =>
|
|
295
|
+
[this.adb.APP_INSTALL_STATE.NOT_INSTALLED, this.adb.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED].includes(
|
|
296
|
+
installState,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
return isAnyComponentInstalled && isAnyComponentNotInstalledOrNewer;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Checks if server components should be installed on the device under test in scope of the current driver session.
|
|
304
|
+
*
|
|
305
|
+
* @param packagesInfo
|
|
306
|
+
* @returns true if any of components is not installed or older than currently installed in order to
|
|
307
|
+
* install or upgrade the servers on the device under test.
|
|
308
|
+
*/
|
|
309
|
+
private shouldInstallServerPackages(packagesInfo: PackageInfo[] = []): boolean {
|
|
310
|
+
return packagesInfo.some(({installState}) =>
|
|
311
|
+
[this.adb.APP_INSTALL_STATE.NOT_INSTALLED, this.adb.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED].includes(
|
|
312
|
+
installState,
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async verifyServicesAvailability(): Promise<void> {
|
|
318
|
+
this.log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT_MS}ms for services to be available`);
|
|
319
|
+
let isPmServiceAvailable = false;
|
|
320
|
+
let pmOutput = '';
|
|
321
|
+
let pmError: Error | null = null;
|
|
322
|
+
try {
|
|
323
|
+
await waitForCondition(
|
|
324
|
+
async () => {
|
|
325
|
+
if (!isPmServiceAvailable) {
|
|
326
|
+
pmError = null;
|
|
327
|
+
pmOutput = '';
|
|
328
|
+
try {
|
|
329
|
+
pmOutput = await this.adb.shell(['pm', 'list', 'instrumentation']);
|
|
330
|
+
} catch (e: any) {
|
|
331
|
+
pmError = e;
|
|
332
|
+
}
|
|
333
|
+
if (pmOutput.includes('Could not access the Package Manager')) {
|
|
334
|
+
pmError = new Error(`Problem running Package Manager: ${pmOutput}`);
|
|
335
|
+
pmOutput = ''; // remove output, so it is not printed below
|
|
336
|
+
} else if (pmOutput.includes(INSTRUMENTATION_TARGET)) {
|
|
337
|
+
pmOutput = ''; // remove output, so it is not printed below
|
|
338
|
+
this.log.debug(`Instrumentation target '${INSTRUMENTATION_TARGET}' is available`);
|
|
339
|
+
isPmServiceAvailable = true;
|
|
340
|
+
} else if (!pmError) {
|
|
341
|
+
pmError = new Error('The instrumentation target is not listed by Package Manager');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return isPmServiceAvailable;
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
waitMs: SERVICES_LAUNCH_TIMEOUT_MS,
|
|
348
|
+
intervalMs: 1000,
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
const errorMessage = (pmError as any)?.message || 'Unknown error';
|
|
353
|
+
this.log.error(
|
|
354
|
+
`Unable to find instrumentation target '${INSTRUMENTATION_TARGET}': ${errorMessage}`,
|
|
355
|
+
);
|
|
356
|
+
if (pmOutput) {
|
|
357
|
+
this.log.debug('Available targets:');
|
|
358
|
+
for (const line of pmOutput.split('\n')) {
|
|
359
|
+
this.log.debug(` ${line.replace('instrumentation:', '')}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async startInstrumentationProcess(): Promise<void> {
|
|
327
366
|
const cmd = ['am', 'instrument', '-w'];
|
|
328
367
|
if (this.disableWindowAnimation) {
|
|
329
368
|
cmd.push('--no-window-animation');
|
|
@@ -335,17 +374,19 @@ export class UiAutomator2Server {
|
|
|
335
374
|
cmd.push('-e', 'disableAnalytics', 'true');
|
|
336
375
|
cmd.push(INSTRUMENTATION_TARGET);
|
|
337
376
|
this.instrumentationProcess = this.adb.createSubProcess(['shell', ...cmd]);
|
|
338
|
-
for (const streamName of ['stderr', 'stdout']) {
|
|
339
|
-
this.instrumentationProcess.on(`line-${streamName}`, (line) =>
|
|
377
|
+
for (const streamName of ['stderr', 'stdout'] as const) {
|
|
378
|
+
this.instrumentationProcess.on(`line-${streamName}`, (line: string) =>
|
|
379
|
+
this.log.debug(`[Instrumentation] ${line}`),
|
|
380
|
+
);
|
|
340
381
|
}
|
|
341
|
-
this.instrumentationProcess.once('exit', (code, signal) => {
|
|
382
|
+
this.instrumentationProcess.once('exit', (code: number | null, signal: string | null) => {
|
|
342
383
|
this.log.debug(`[Instrumentation] The process has exited with code ${code}, signal ${signal}`);
|
|
343
384
|
this.jwproxy.didInstrumentationExit = true;
|
|
344
385
|
});
|
|
345
386
|
await this.instrumentationProcess.start(0);
|
|
346
387
|
}
|
|
347
388
|
|
|
348
|
-
async stopInstrumentationProcess
|
|
389
|
+
private async stopInstrumentationProcess(): Promise<void> {
|
|
349
390
|
try {
|
|
350
391
|
if (this.instrumentationProcess?.isRunning) {
|
|
351
392
|
await this.instrumentationProcess.stop();
|
|
@@ -356,69 +397,39 @@ export class UiAutomator2Server {
|
|
|
356
397
|
}
|
|
357
398
|
}
|
|
358
399
|
|
|
359
|
-
async
|
|
360
|
-
this.log.debug('Deleting UiAutomator2 server session');
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
await this.jwproxy.command('/', 'DELETE');
|
|
364
|
-
} catch (err) {
|
|
365
|
-
this.log.warn(
|
|
366
|
-
`Did not get the confirmation of UiAutomator2 server session deletion. ` +
|
|
367
|
-
`Original error: ${err.message}`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Theoretically we could also force kill instumentation and server processes
|
|
372
|
-
// without waiting for them to properly quit on their own.
|
|
373
|
-
// This may cause unexpected error reports in device logs though.
|
|
374
|
-
await this._waitForTermination();
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
await this.stopInstrumentationProcess();
|
|
378
|
-
} catch (err) {
|
|
379
|
-
this.log.warn(`Could not stop the instrumentation process. Original error: ${err.message}`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
await B.all([
|
|
384
|
-
this.adb.forceStop(SERVER_PACKAGE_ID),
|
|
385
|
-
this.adb.forceStop(SERVER_TEST_PACKAGE_ID)
|
|
386
|
-
]);
|
|
387
|
-
} catch {}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async cleanupAutomationLeftovers (strictCleanup = false) {
|
|
400
|
+
private async cleanupAutomationLeftovers(strictCleanup: boolean = false): Promise<void> {
|
|
391
401
|
this.log.debug(`Performing ${strictCleanup ? 'strict' : 'shallow'} cleanup of automation leftovers`);
|
|
392
402
|
|
|
393
403
|
const serverBase = `http://${this.host}:${this.systemPort}`;
|
|
394
404
|
try {
|
|
395
|
-
const {value} = (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
405
|
+
const {value} = (
|
|
406
|
+
await axios({
|
|
407
|
+
url: `${serverBase}/sessions`,
|
|
408
|
+
timeout: SERVER_REQUEST_TIMEOUT_MS,
|
|
409
|
+
})
|
|
410
|
+
).data as SessionsResponse;
|
|
399
411
|
const activeSessionIds = value.map(({id}) => id).filter(Boolean);
|
|
400
412
|
if (activeSessionIds.length) {
|
|
401
413
|
this.log.debug(`The following obsolete sessions are still running: ${activeSessionIds}`);
|
|
402
414
|
this.log.debug(`Cleaning up ${util.pluralize('obsolete session', activeSessionIds.length, true)}`);
|
|
403
|
-
await B.all(
|
|
404
|
-
.map((
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
await B.all(
|
|
416
|
+
activeSessionIds.map((id: string) =>
|
|
417
|
+
axios.delete(`${serverBase}/session/${id}`, {
|
|
418
|
+
timeout: SERVER_REQUEST_TIMEOUT_MS,
|
|
419
|
+
}),
|
|
420
|
+
),
|
|
407
421
|
);
|
|
408
422
|
// Let the server to be properly terminated before continuing
|
|
409
423
|
await this._waitForTermination();
|
|
410
424
|
} else {
|
|
411
425
|
this.log.debug('No obsolete sessions have been detected');
|
|
412
426
|
}
|
|
413
|
-
} catch (e) {
|
|
427
|
+
} catch (e: any) {
|
|
414
428
|
this.log.debug(`No obsolete sessions have been detected (${e.message})`);
|
|
415
429
|
}
|
|
416
430
|
|
|
417
431
|
try {
|
|
418
|
-
await B.all([
|
|
419
|
-
this.adb.forceStop(SERVER_PACKAGE_ID),
|
|
420
|
-
this.adb.forceStop(SERVER_TEST_PACKAGE_ID)
|
|
421
|
-
]);
|
|
432
|
+
await B.all([this.adb.forceStop(SERVER_PACKAGE_ID), this.adb.forceStop(SERVER_TEST_PACKAGE_ID)]);
|
|
422
433
|
} catch {}
|
|
423
434
|
if (strictCleanup) {
|
|
424
435
|
// https://github.com/appium/appium/issues/10749
|
|
@@ -434,22 +445,25 @@ export class UiAutomator2Server {
|
|
|
434
445
|
*
|
|
435
446
|
* @returns {Promise<void>}
|
|
436
447
|
*/
|
|
437
|
-
async _waitForTermination() {
|
|
448
|
+
private async _waitForTermination(): Promise<void> {
|
|
438
449
|
try {
|
|
439
|
-
await waitForCondition(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
await waitForCondition(
|
|
451
|
+
async () => {
|
|
452
|
+
try {
|
|
453
|
+
return !(await this.adb.processExists(SERVER_PACKAGE_ID));
|
|
454
|
+
} catch {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
waitMs: SERVER_SHUTDOWN_TIMEOUT_MS,
|
|
460
|
+
intervalMs: 300,
|
|
461
|
+
},
|
|
462
|
+
);
|
|
449
463
|
} catch {
|
|
450
464
|
this.log.warn(
|
|
451
|
-
`The UIA2 server
|
|
452
|
-
|
|
465
|
+
`The UIA2 server has not been terminated within ${SERVER_SHUTDOWN_TIMEOUT_MS}ms timeout. ` +
|
|
466
|
+
`Continuing anyway`,
|
|
453
467
|
);
|
|
454
468
|
}
|
|
455
469
|
}
|
|
@@ -457,16 +471,32 @@ export class UiAutomator2Server {
|
|
|
457
471
|
|
|
458
472
|
export default UiAutomator2Server;
|
|
459
473
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
474
|
+
export interface PackageInfo {
|
|
475
|
+
installState: InstallState;
|
|
476
|
+
appPath: string;
|
|
477
|
+
appId: string;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface UiAutomator2ServerOptions {
|
|
481
|
+
adb: ADB;
|
|
482
|
+
host: string;
|
|
483
|
+
systemPort: number;
|
|
484
|
+
disableWindowAnimation: boolean;
|
|
485
|
+
readTimeout?: number;
|
|
486
|
+
disableSuppressAccessibilityService?: boolean;
|
|
487
|
+
basePath?: string;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Type helper to extract required (non-optional) keys from UiAutomator2ServerOptions
|
|
491
|
+
type RequiredKeysOf<T> = {
|
|
492
|
+
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
|
493
|
+
}[keyof T];
|
|
494
|
+
|
|
495
|
+
interface SessionInfo {
|
|
496
|
+
id: string;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface SessionsResponse {
|
|
500
|
+
value: SessionInfo[];
|
|
501
|
+
}
|
|
502
|
+
|