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.
@@ -1,16 +1,25 @@
1
1
  import _ from 'lodash';
2
- import { JWProxy, errors } from 'appium/driver';
3
- import { waitForCondition } from 'asyncbox';
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 { util, timing } from 'appium/support';
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
- /** @type {boolean} */
25
- didInstrumentationExit;
39
+ public didInstrumentationExit: boolean = false;
26
40
 
27
- /**
28
- * @override
29
- */
30
- async proxyCommand (url, method, body = null) {
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
- 'the instrumentation process is not running (probably crashed). ' +
35
- 'Check the server log and/or the logcat output for more details');
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
- /** @type {string} */
43
- host;
44
-
45
- /** @type {number} */
46
- systemPort;
47
-
48
- /** @type {import('appium-adb').ADB} */
49
- adb;
50
-
51
- /** @type {boolean} */
52
- disableWindowAnimation;
53
-
54
- /** @type {boolean|undefined} */
55
- disableSuppressAccessibilityService;
56
-
57
- /** @type {import('teen_process').SubProcess|null} */
58
- instrumentationProcess;
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[req] = opts[req];
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 {number} installTimeout - Installation timeout
103
+ * @param installTimeout - Installation timeout
165
104
  */
166
- async installServerApk (installTimeout = SERVER_INSTALL_RETRIES * 1000) {
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 verifyServicesAvailability () {
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(async () => {
280
- try {
281
- await this.jwproxy.command('/status', 'GET');
282
- return true;
283
- } catch {
284
- // short circuit to retry or fail fast
285
- return this.jwproxy.didInstrumentationExit;
286
- }
287
- }, {
288
- waitMs: timeout,
289
- intervalMs: 1000,
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
- + 'Make sure the application under test does not crash and investigate the logcat output. '
295
- + `You could also try to increase the value of 'uiautomator2ServerLaunchTimeout' capability`
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
- + 'Make sure the application under test does not crash and investigate the logcat output.'
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(`The instrumentation process has been unexpectedly terminated. `
311
- + `Retrying UiAutomator2 startup (#${retries} of ${maxRetries - 1})`);
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(`The initialization of the instrumentation process took `
317
- + `${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
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 startInstrumentationProcess () {
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) => this.log.debug(`[Instrumentation] ${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 deleteSession () {
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} = (await axios({
396
- url: `${serverBase}/sessions`,
397
- timeout: SERVER_REQUEST_TIMEOUT_MS,
398
- })).data;
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(activeSessionIds
404
- .map((/** @type {string} */ id) => axios.delete(`${serverBase}/session/${id}`, {
405
- timeout: SERVER_REQUEST_TIMEOUT_MS,
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(async () => {
440
- try {
441
- return !(await this.adb.processExists(SERVER_PACKAGE_ID));
442
- } catch {
443
- return true;
444
- }
445
- }, {
446
- waitMs: SERVER_SHUTDOWN_TIMEOUT_MS,
447
- intervalMs: 300,
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 did has not been terminated within ${SERVER_SHUTDOWN_TIMEOUT_MS}ms timeout. ` +
452
- `Continuing anyway`
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
- * @typedef {Object} UiAutomator2ServerOptions
462
- * @property {import('appium-adb').ADB} adb
463
- * @property {string} tmpDir
464
- * @property {string} host
465
- * @property {number} systemPort
466
- * @property {number} devicePort
467
- * @property {boolean} disableWindowAnimation
468
- * @property {number} [readTimeout]
469
- * @property {boolean} [disableSuppressAccessibilityService]
470
- * @property {string} [apk]
471
- * @property {string} [basePath]
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
+