appium-adb 14.0.2 → 14.0.4

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.
@@ -4,12 +4,23 @@ import B from 'bluebird';
4
4
  import { system, fs, util, tempDir, timing } from '@appium/support';
5
5
  import {
6
6
  DEFAULT_ADB_EXEC_TIMEOUT, getSdkRootFromEnv
7
- } from '../helpers';
8
- import { exec, SubProcess } from 'teen_process';
9
- import { sleep, retry, retryInterval, waitForCondition } from 'asyncbox';
7
+ } from '../helpers.js';
8
+ import { exec, SubProcess, type ExecError } from 'teen_process';
9
+ import { retry, retryInterval, waitForCondition } from 'asyncbox';
10
10
  import _ from 'lodash';
11
11
  import * as semver from 'semver';
12
-
12
+ import type { ADB } from '../adb.js';
13
+ import type {
14
+ ConnectedDevicesOptions,
15
+ Device,
16
+ AvdLaunchOptions,
17
+ Version,
18
+ RootResult,
19
+ ShellExecOptions,
20
+ SpecialAdbExecOptions,
21
+ TFullOutputOption,
22
+ ExecResult,
23
+ } from './types.js';
13
24
 
14
25
  const DEFAULT_ADB_REBOOT_RETRIES = 90;
15
26
  const LINKER_WARNING_REGEXP = /^WARNING: linker.+$/m;
@@ -17,11 +28,11 @@ const ADB_RETRY_ERROR_PATTERNS = [
17
28
  /protocol fault \(no status\)/i,
18
29
  /error: device ('.+' )?not found/i,
19
30
  /error: device still connecting/i,
20
- ];
31
+ ] as const;
21
32
  const BINARY_VERSION_PATTERN = /^Version ([\d.]+)-(\d+)/m;
22
33
  const BRIDGE_VERSION_PATTERN = /^Android Debug Bridge version ([\d.]+)/m;
23
34
  const CERTS_ROOT = '/system/etc/security/cacerts';
24
- const SDK_BINARY_ROOTS = [
35
+ const SDK_BINARY_ROOTS: (string | string[])[] = [
25
36
  'platform-tools',
26
37
  'emulator',
27
38
  ['cmdline-tools', 'latest', 'bin'],
@@ -30,28 +41,18 @@ const SDK_BINARY_ROOTS = [
30
41
  '.' // Allow custom sdkRoot to specify full folder path
31
42
  ];
32
43
  const MIN_DELAY_ADB_API_LEVEL = 28;
33
- const REQUIRED_SERVICES = ['activity', 'package', 'window'];
44
+ const REQUIRED_SERVICES = ['activity', 'package', 'window'] as const;
34
45
  const MAX_SHELL_BUFFER_LENGTH = 1000;
35
46
 
36
- /**
37
- * Retrieve full path to the given binary.
38
- *
39
- * @this {import('../adb.js').ADB}
40
- * @param {string} binaryName - The name of the binary.
41
- * @return {Promise<string>} Full path to the given binary including current SDK root.
42
- */
43
- export async function getSdkBinaryPath (binaryName) {
44
- return await this.getBinaryFromSdkRoot(binaryName);
45
- }
47
+ // Private methods (defined early as they're used by public methods)
46
48
 
47
49
  /**
48
50
  * Retrieve full binary name for the current operating system.
49
51
  *
50
- * @param {string} binaryName - simple binary name, for example 'android'.
51
- * @return {string} Formatted binary name depending on the current platform,
52
- * for example, 'android.bat' on Windows.
52
+ * @param binaryName - The name of the binary
53
+ * @returns The binary name with appropriate extension for the current OS
53
54
  */
54
- function _getBinaryNameForOS (binaryName) {
55
+ function _getBinaryNameForOS (binaryName: string): string {
55
56
  if (!system.isWindows()) {
56
57
  return binaryName;
57
58
  }
@@ -65,33 +66,67 @@ function _getBinaryNameForOS (binaryName) {
65
66
  return binaryName;
66
67
  }
67
68
 
69
+ /**
70
+ * Returns the Android binaries locations
71
+ *
72
+ * @param sdkRoot - The Android SDK root directory path
73
+ * @param fullBinaryName - The full name of the binary (with extension)
74
+ * @returns Array of possible binary location paths
75
+ */
76
+ function getSdkBinaryLocationCandidates (sdkRoot: string, fullBinaryName: string): string[] {
77
+ return SDK_BINARY_ROOTS.map((x) =>
78
+ path.resolve(sdkRoot, ...(_.isArray(x) ? x : [x]), fullBinaryName));
79
+ }
80
+
81
+ /**
82
+ * Get the path to the openssl binary for the current operating system
83
+ *
84
+ * @returns The full path to the openssl binary
85
+ * @throws {Error} If openssl is not found in PATH
86
+ */
87
+ async function getOpenSslForOs (): Promise<string> {
88
+ const binaryName = `openssl${system.isWindows() ? '.exe' : ''}`;
89
+ try {
90
+ return await fs.which(binaryName);
91
+ } catch {
92
+ throw new Error('The openssl tool must be installed on the system and available on the path');
93
+ }
94
+ }
95
+
96
+ // Public methods
97
+
98
+ /**
99
+ * Retrieve full path to the given binary.
100
+ *
101
+ * @param binaryName - The name of the binary
102
+ * @returns The full path to the binary
103
+ */
104
+ export async function getSdkBinaryPath (this: ADB, binaryName: string): Promise<string> {
105
+ return await this.getBinaryFromSdkRoot(binaryName);
106
+ }
107
+
68
108
  export const getBinaryNameForOS = _.memoize(_getBinaryNameForOS);
69
109
 
70
110
  /**
71
111
  * Retrieve full path to the given binary and caches it into `binaries`
72
112
  * property of the current ADB instance.
73
113
  *
74
- * @this {import('../adb.js').ADB}
75
- * @param {string} binaryName - Simple name of a binary file.
76
- * @return {Promise<string>} Full path to the given binary. The method tries
77
- * to enumerate all the known locations where the binary
78
- * might be located and stops the search as soon as the first
79
- * match is found on the local file system.
80
- * @throws {Error} If the binary with given name is not present at any
81
- * of known locations or Android SDK is not installed on the
82
- * local file system.
114
+ * @param binaryName - The name of the binary
115
+ * @returns The full path to the binary
116
+ * @throws {Error} If SDK root is not set or binary cannot be found
83
117
  */
84
- export async function getBinaryFromSdkRoot (binaryName) {
85
- if ((/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName]) {
86
- return (/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName];
118
+ export async function getBinaryFromSdkRoot (this: ADB, binaryName: string): Promise<string> {
119
+ if (this.binaries?.[binaryName]) {
120
+ return this.binaries[binaryName];
87
121
  }
88
122
  const fullBinaryName = this.getBinaryNameForOS(binaryName);
89
- const binaryLocs = getSdkBinaryLocationCandidates(
90
- /** @type {string} */(this.sdkRoot), fullBinaryName
91
- );
123
+ if (!this.sdkRoot) {
124
+ throw new Error('SDK root is not set');
125
+ }
126
+ const binaryLocs = getSdkBinaryLocationCandidates(this.sdkRoot, fullBinaryName);
92
127
 
93
128
  // get subpaths for currently installed build tool directories
94
- let buildToolsDirs = await getBuildToolsDirs(/** @type {string} */(this.sdkRoot));
129
+ let buildToolsDirs = await getBuildToolsDirs(this.sdkRoot);
95
130
  if (this.buildToolsVersion) {
96
131
  buildToolsDirs = buildToolsDirs
97
132
  .filter((x) => path.basename(x) === this.buildToolsVersion);
@@ -108,7 +143,7 @@ export async function getBinaryFromSdkRoot (binaryName) {
108
143
  ]))
109
144
  ));
110
145
 
111
- let binaryLoc = null;
146
+ let binaryLoc: string | null = null;
112
147
  for (const loc of binaryLocs) {
113
148
  if (await fs.exists(loc)) {
114
149
  binaryLoc = loc;
@@ -121,39 +156,22 @@ export async function getBinaryFromSdkRoot (binaryName) {
121
156
  `installed at '${this.sdkRoot}'?`);
122
157
  }
123
158
  log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`);
124
- (/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName] = binaryLoc;
159
+ if (!this.binaries) {
160
+ this.binaries = {};
161
+ }
162
+ this.binaries[binaryName] = binaryLoc;
125
163
  return binaryLoc;
126
164
  }
127
165
 
128
- /**
129
- * Returns the Android binaries locations
130
- *
131
- * @param {string} sdkRoot The path to Android SDK root.
132
- * @param {string} fullBinaryName The name of full binary name.
133
- * @return {string[]} The list of SDK_BINARY_ROOTS paths
134
- * with sdkRoot and fullBinaryName.
135
- */
136
- function getSdkBinaryLocationCandidates (sdkRoot, fullBinaryName) {
137
- return SDK_BINARY_ROOTS.map((x) =>
138
- path.resolve(sdkRoot, ...(_.isArray(x) ? x : [x]), fullBinaryName));
139
- }
140
-
141
166
  /**
142
167
  * Retrieve full path to the given binary.
143
168
  * This method does not have cache.
144
169
  *
145
- * @param {string} binaryName - Simple name of a binary file.
146
- * e.g. 'adb', 'android'
147
- * @return {Promise<string>} Full path to the given binary. The method tries
148
- * to enumerate all the known locations where the binary
149
- * might be located and stops the search as soon as the first
150
- * match is found on the local file system.
151
- * e.g. '/Path/To/Android/sdk/platform-tools/adb'
152
- * @throws {Error} If the binary with given name is not present at any
153
- * of known locations or Android SDK is not installed on the
154
- * local file system.
170
+ * @param binaryName - The name of the binary
171
+ * @returns The full path to the binary
172
+ * @throws {Error} If binary cannot be found in the Android SDK
155
173
  */
156
- export async function getAndroidBinaryPath (binaryName) {
174
+ export async function getAndroidBinaryPath (binaryName: string): Promise<string> {
157
175
  const fullBinaryName = getBinaryNameForOS(binaryName);
158
176
  const sdkRoot = getSdkRootFromEnv();
159
177
  const binaryLocs = getSdkBinaryLocationCandidates(sdkRoot ?? '', fullBinaryName);
@@ -169,22 +187,23 @@ export async function getAndroidBinaryPath (binaryName) {
169
187
  /**
170
188
  * Retrieve full path to a binary file using the standard system lookup tool.
171
189
  *
172
- * @this {import('../adb.js').ADB}
173
- * @param {string} binaryName - The name of the binary.
174
- * @return {Promise<string>} Full path to the binary received from 'which'/'where'
175
- * output.
176
- * @throws {Error} If lookup tool returns non-zero return code.
190
+ * @param binaryName - The name of the binary
191
+ * @returns The full path to the binary
192
+ * @throws {Error} If binary cannot be found in PATH
177
193
  */
178
- export async function getBinaryFromPath (binaryName) {
179
- if ((/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName]) {
180
- return (/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName];
194
+ export async function getBinaryFromPath (this: ADB, binaryName: string): Promise<string> {
195
+ if (this.binaries?.[binaryName]) {
196
+ return this.binaries[binaryName];
181
197
  }
182
198
 
183
199
  const fullBinaryName = this.getBinaryNameForOS(binaryName);
184
200
  try {
185
201
  const binaryLoc = await fs.which(fullBinaryName);
186
202
  log.info(`Using '${fullBinaryName}' from '${binaryLoc}'`);
187
- (/** @type {import('./types').StringRecord} */ (this.binaries))[binaryName] = binaryLoc;
203
+ if (!this.binaries) {
204
+ this.binaries = {};
205
+ }
206
+ this.binaries[binaryName] = binaryLoc;
188
207
  return binaryLoc;
189
208
  } catch {
190
209
  throw new Error(`Could not find '${fullBinaryName}' in PATH. Please set the ANDROID_HOME ` +
@@ -195,24 +214,23 @@ export async function getBinaryFromPath (binaryName) {
195
214
  /**
196
215
  * Retrieve the list of devices visible to adb.
197
216
  *
198
- * @this {import('../adb.js').ADB}
199
- * @param {import('./types').ConnectedDevicesOptions} [opts={}] - Additional options mapping.
200
- * @return {Promise<import('./types').Device[]>} The list of devices or an empty list if
201
- * no devices are connected.
202
- * @throws {Error} If there was an error while listing devices.
217
+ * @param opts - Options for device retrieval
218
+ * @returns Array of connected devices
219
+ * @throws {Error} If adb devices command fails or returns unexpected output
203
220
  */
204
- export async function getConnectedDevices (opts = {}) {
221
+ export async function getConnectedDevices (this: ADB, opts: ConnectedDevicesOptions = {}): Promise<Device[]> {
205
222
  log.debug('Getting connected devices');
206
223
  const args = [...this.executable.defaultArgs, 'devices'];
207
224
  if (opts.verbose) {
208
225
  args.push('-l');
209
226
  }
210
227
 
211
- let stdout;
228
+ let stdout: string;
212
229
  try {
213
230
  ({stdout} = await exec(this.executable.path, args));
214
- } catch (e) {
215
- throw new Error(`Error while getting connected devices. Original error: ${e.message}`);
231
+ } catch (e: unknown) {
232
+ const error = e as Error;
233
+ throw new Error(`Error while getting connected devices. Original error: ${error.message}`);
216
234
  }
217
235
  const listHeader = 'List of devices';
218
236
  // expecting adb devices to return output as
@@ -224,7 +242,7 @@ export async function getConnectedDevices (opts = {}) {
224
242
  }
225
243
  // slicing output we care about
226
244
  stdout = stdout.slice(startingIndex);
227
- let excludedLines = [listHeader, 'adb server', '* daemon'];
245
+ const excludedLines = [listHeader, 'adb server', '* daemon'];
228
246
  if (!this.allowOfflineDevices) {
229
247
  excludedLines.push('offline');
230
248
  }
@@ -234,7 +252,7 @@ export async function getConnectedDevices (opts = {}) {
234
252
  .map((line) => {
235
253
  // state is "device", afaic
236
254
  const [udid, state, ...description] = line.split(/\s+/);
237
- const device = {udid, state};
255
+ const device: Device & Record<string, string> = {udid, state} as Device & Record<string, string>;
238
256
  if (opts.verbose) {
239
257
  for (const entry of description) {
240
258
  if (entry.includes(':')) {
@@ -257,16 +275,14 @@ export async function getConnectedDevices (opts = {}) {
257
275
  /**
258
276
  * Retrieve the list of devices visible to adb within the given timeout.
259
277
  *
260
- * @this {import('../adb.js').ADB}
261
- * @param {number} timeoutMs - The maximum number of milliseconds to get at least
262
- * one list item.
263
- * @return {Promise<import('./types').Device[]>} The list of connected devices.
264
- * @throws {Error} If no connected devices can be detected within the given timeout.
278
+ * @param timeoutMs - Maximum time to wait for devices (default: 20000ms)
279
+ * @returns Array of connected devices
280
+ * @throws {Error} If no devices are found within the timeout period
265
281
  */
266
- export async function getDevicesWithRetry (timeoutMs = 20000) {
282
+ export async function getDevicesWithRetry (this: ADB, timeoutMs: number = 20000): Promise<Device[]> {
267
283
  log.debug('Trying to find connected Android devices');
268
284
  try {
269
- let devices;
285
+ let devices: Device[] = [];
270
286
  await waitForCondition(async () => {
271
287
  try {
272
288
  devices = await this.getConnectedDevices();
@@ -274,9 +290,10 @@ export async function getDevicesWithRetry (timeoutMs = 20000) {
274
290
  return true;
275
291
  }
276
292
  log.debug('Could not find online devices');
277
- } catch (err) {
278
- log.debug(err.stack);
279
- log.warn(`Got an unexpected error while fetching connected devices list: ${err.message}`);
293
+ } catch (err: unknown) {
294
+ const error = err as Error;
295
+ log.debug(error.stack);
296
+ log.warn(`Got an unexpected error while fetching connected devices list: ${error.message}`);
280
297
  }
281
298
 
282
299
  try {
@@ -289,9 +306,10 @@ export async function getDevicesWithRetry (timeoutMs = 20000) {
289
306
  waitMs: timeoutMs,
290
307
  intervalMs: 200,
291
308
  });
292
- return /** @type {any} */ (devices);
293
- } catch (e) {
294
- if (/Condition unmet/.test(e.message)) {
309
+ return devices;
310
+ } catch (e: unknown) {
311
+ const error = e as Error;
312
+ if (/Condition unmet/.test(error.message)) {
295
313
  throw new Error(`Could not find a connected Android device in ${timeoutMs}ms`);
296
314
  } else {
297
315
  throw e;
@@ -302,15 +320,10 @@ export async function getDevicesWithRetry (timeoutMs = 20000) {
302
320
  /**
303
321
  * Kick current connection from host/device side and make it reconnect
304
322
  *
305
- * @this {import('../adb.js').ADB}
306
- * @param {string} [target=offline] One of possible targets to reconnect:
307
- * offline, device or null
308
- * Providing `null` will cause reconnection to happen from the host side.
309
- *
310
- * @throws {Error} If either ADB version is too old and does not support this
311
- * command or there was a failure during reconnect.
323
+ * @param target - The target to reconnect (default: 'offline')
324
+ * @throws {Error} If reconnect command fails
312
325
  */
313
- export async function reconnect (target = 'offline') {
326
+ export async function reconnect (this: ADB, target: string | null = 'offline'): Promise<void> {
314
327
  log.debug(`Reconnecting adb (target ${target})`);
315
328
 
316
329
  const args = ['reconnect'];
@@ -319,17 +332,16 @@ export async function reconnect (target = 'offline') {
319
332
  }
320
333
  try {
321
334
  await this.adbExec(args);
322
- } catch (e) {
323
- throw new Error(`Cannot reconnect adb. Original error: ${e.stderr || e.message}`);
335
+ } catch (e: unknown) {
336
+ const error = e as ExecError;
337
+ throw new Error(`Cannot reconnect adb. Original error: ${error.stderr || error.message}`);
324
338
  }
325
339
  }
326
340
 
327
341
  /**
328
342
  * Restart adb server, unless _this.suppressKillServer_ property is true.
329
- *
330
- * @this {import('../adb.js').ADB}
331
343
  */
332
- export async function restartAdb () {
344
+ export async function restartAdb (this: ADB): Promise<void> {
333
345
  if (this.suppressKillServer) {
334
346
  log.debug(`Not restarting abd since 'suppressKillServer' is on`);
335
347
  return;
@@ -346,9 +358,8 @@ export async function restartAdb () {
346
358
 
347
359
  /**
348
360
  * Kill adb server.
349
- * @this {import('../adb.js').ADB}
350
361
  */
351
- export async function killServer () {
362
+ export async function killServer (this: ADB): Promise<void> {
352
363
  log.debug(`Killing adb server on port '${this.adbPort}'`);
353
364
  await this.adbExec(['kill-server'], {
354
365
  exclusive: true,
@@ -359,10 +370,9 @@ export async function killServer () {
359
370
  * Reset Telnet authentication token.
360
371
  * @see {@link http://tools.android.com/recent/emulator2516releasenotes} for more details.
361
372
  *
362
- * @this {import('../adb.js').ADB}
363
- * @returns {Promise<boolean>} If token reset was successful.
373
+ * @returns True if token was reset successfully, false otherwise
364
374
  */
365
- export const resetTelnetAuthToken = _.memoize(async function resetTelnetAuthToken () {
375
+ export const resetTelnetAuthToken = _.memoize(async function resetTelnetAuthToken (): Promise<boolean> {
366
376
  // The methods is used to remove telnet auth token
367
377
  //
368
378
  const homeFolderPath = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
@@ -374,8 +384,9 @@ export const resetTelnetAuthToken = _.memoize(async function resetTelnetAuthToke
374
384
  log.debug(`Overriding ${dstPath} with an empty string to avoid telnet authentication for emulator commands`);
375
385
  try {
376
386
  await fs.writeFile(dstPath, '');
377
- } catch (e) {
378
- log.warn(`Error ${e.message} while resetting the content of ${dstPath}. Ignoring resetting of emulator's telnet authentication token`);
387
+ } catch (e: unknown) {
388
+ const error = e as Error;
389
+ log.warn(`Error ${error.message} while resetting the content of ${dstPath}. Ignoring resetting of emulator's telnet authentication token`);
379
390
  return false;
380
391
  }
381
392
  return true;
@@ -384,10 +395,10 @@ export const resetTelnetAuthToken = _.memoize(async function resetTelnetAuthToke
384
395
  /**
385
396
  * Execute the given emulator command using _adb emu_ tool.
386
397
  *
387
- * @this {import('../adb.js').ADB}
388
- * @param {string[]} cmd - The array of rest command line parameters.
398
+ * @param cmd - Array of command arguments
399
+ * @throws {Error} If emulator is not connected or command execution fails
389
400
  */
390
- export async function adbExecEmu (cmd) {
401
+ export async function adbExecEmu (this: ADB, cmd: string[]): Promise<void> {
391
402
  await this.verifyEmulatorConnected();
392
403
  await this.resetTelnetAuthToken();
393
404
  await this.adbExec(['emu', ...cmd]);
@@ -395,38 +406,29 @@ export async function adbExecEmu (cmd) {
395
406
 
396
407
  let isExecLocked = false;
397
408
 
398
- /** @type {{STDOUT: 'stdout', FULL: 'full'}} */
399
- export const EXEC_OUTPUT_FORMAT = Object.freeze({
409
+ export const EXEC_OUTPUT_FORMAT = {
400
410
  STDOUT: 'stdout',
401
411
  FULL: 'full',
402
- });
412
+ } as const;
403
413
 
404
414
  /**
405
415
  * Execute the given adb command.
406
416
  *
407
- * @template {import('teen_process').TeenProcessExecOptions & import('./types').ShellExecOptions & import('./types').SpecialAdbExecOptions} TExecOpts
408
- * @this {import('../adb.js').ADB}
409
- * @param {string|string[]} cmd - The array of rest command line parameters
410
- * or a single string parameter.
411
- * @param {TExecOpts} [opts] Additional options mapping. See
412
- * {@link https://github.com/appium/node-teen_process}
413
- * for more details.
414
- * You can also set the additional `exclusive` param
415
- * to `true` that assures no other parallel adb commands
416
- * are going to be executed while the current one is running
417
- * You can set the `outputFormat` param to `stdout` to receive just the stdout
418
- * output (default) or `full` to receive the stdout and stderr response from a
419
- * command with a zero exit code
420
- * @return {Promise<TExecOpts extends import('./types').TFullOutputOption ? import('teen_process').TeenProcessExecResult : string>}
421
- * Command's stdout or an object containing stdout and stderr.
422
- * @throws {Error} If the command returned non-zero exit code.
417
+ * @param cmd - Command string or array of command arguments
418
+ * @param opts - Execution options
419
+ * @returns Command output (string or ExecResult depending on outputFormat)
420
+ * @throws {Error} If command execution fails or timeout is exceeded
423
421
  */
424
- export async function adbExec (cmd, opts) {
422
+ export async function adbExec<TExecOpts extends ShellExecOptions & SpecialAdbExecOptions = ShellExecOptions & SpecialAdbExecOptions>(
423
+ this: ADB,
424
+ cmd: string | string[],
425
+ opts?: TExecOpts
426
+ ): Promise<TExecOpts extends TFullOutputOption ? ExecResult : string> {
425
427
  if (!cmd) {
426
428
  throw new Error('You need to pass in a command to adbExec()');
427
429
  }
428
430
 
429
- const optsCopy = _.cloneDeep(opts) ?? /** @type {TExecOpts} */ ({});
431
+ const optsCopy = _.cloneDeep(opts ?? {}) as TExecOpts;
430
432
  // setting default timeout for each command to prevent infinite wait.
431
433
  optsCopy.timeout = optsCopy.timeout || this.adbExecTimeout || DEFAULT_ADB_EXEC_TIMEOUT;
432
434
  optsCopy.timeoutCapName = optsCopy.timeoutCapName || 'adbExecTimeout'; // For error message
@@ -435,43 +437,43 @@ export async function adbExec (cmd, opts) {
435
437
 
436
438
  cmd = _.isArray(cmd) ? cmd : [cmd];
437
439
  let adbRetried = false;
438
- const execFunc = async () => {
440
+ const execFunc = async (): Promise<string | ExecResult> => {
439
441
  try {
440
442
  const args = [...this.executable.defaultArgs, ...cmd];
441
443
  log.debug(`Running '${this.executable.path} ` +
442
444
  (args.find((arg) => /\s+/.test(arg)) ? util.quote(args) : args.join(' ')) + `'`);
443
- let {stdout, stderr} = await exec(this.executable.path, args, optsCopy);
445
+ const {stdout: rawStdout, stderr} = await exec(this.executable.path, args, optsCopy);
444
446
  // sometimes ADB prints out weird stdout warnings that we don't want
445
447
  // to include in any of the response data, so let's strip it out
446
- stdout = stdout.replace(LINKER_WARNING_REGEXP, '').trim();
448
+ const stdout = rawStdout.replace(LINKER_WARNING_REGEXP, '').trim();
447
449
  return outputFormat === this.EXEC_OUTPUT_FORMAT.FULL ? {stdout, stderr} : stdout;
448
- } catch (e) {
449
- const errText = `${e.message}, ${e.stdout}, ${e.stderr}`;
450
+ } catch (e: unknown) {
451
+ const error = e as ExecError;
452
+ const errText = `${error.message}, ${error.stdout}, ${error.stderr}`;
450
453
  if (ADB_RETRY_ERROR_PATTERNS.some((p) => p.test(errText))) {
451
454
  log.info(`Error sending command, reconnecting device and retrying: ${cmd}`);
452
- await sleep(1000);
453
455
  await this.getDevicesWithRetry();
454
456
 
455
457
  // try again one time
456
- if (adbRetried) {
458
+ if (!adbRetried) {
457
459
  adbRetried = true;
458
460
  return await execFunc();
459
461
  }
460
462
  }
461
463
 
462
- if (e.code === 0 && e.stdout) {
463
- return e.stdout.replace(LINKER_WARNING_REGEXP, '').trim();
464
+ if (error.code === 0 && error.stdout) {
465
+ return error.stdout.replace(LINKER_WARNING_REGEXP, '').trim();
464
466
  }
465
467
 
466
- if (_.isNull(e.code)) {
467
- e.message = `Error executing adbExec. Original error: '${e.message}'. ` +
468
+ if (_.isNull(error.code)) {
469
+ error.message = `Error executing adbExec. Original error: '${error.message}'. ` +
468
470
  `Try to increase the ${optsCopy.timeout}ms adb execution timeout ` +
469
471
  `represented by '${optsCopy.timeoutCapName}' capability`;
470
472
  } else {
471
- e.message = `Error executing adbExec. Original error: '${e.message}'; ` +
472
- `Command output: ${e.stderr || e.stdout || '<empty>'}`;
473
+ error.message = `Error executing adbExec. Original error: '${error.message}'; ` +
474
+ `Command output: ${error.stderr || error.stdout || '<empty>'}`;
473
475
  }
474
- throw e;
476
+ throw error;
475
477
  }
476
478
  };
477
479
 
@@ -487,7 +489,7 @@ export async function adbExec (cmd, opts) {
487
489
  isExecLocked = true;
488
490
  }
489
491
  try {
490
- return await execFunc();
492
+ return await execFunc() as TExecOpts extends TFullOutputOption ? ExecResult : string;
491
493
  } finally {
492
494
  if (optsCopy.exclusive) {
493
495
  isExecLocked = false;
@@ -498,22 +500,22 @@ export async function adbExec (cmd, opts) {
498
500
  /**
499
501
  * Execute the given command using _adb shell_ prefix.
500
502
  *
501
- * @this {import('../adb.js').ADB}
502
- * @template {import('./types').ShellExecOptions} TShellExecOpts
503
- * @param {string|string[]} cmd - The array of rest command line parameters or a single
504
- * string parameter.
505
- * @param {TShellExecOpts} [opts] - Additional options mapping.
506
- * @return {Promise<TShellExecOpts extends import('./types').TFullOutputOption ? import('teen_process').TeenProcessExecResult : string>}
507
- * Command's stdout.
508
- * @throws {Error} If the command returned non-zero exit code.
503
+ * @param cmd - Command string or array of command arguments
504
+ * @param opts - Execution options
505
+ * @returns Command output (string or ExecResult depending on outputFormat)
506
+ * @throws {Error} If command execution fails
509
507
  */
510
- export async function shell (cmd, opts) {
508
+ export async function shell<TShellExecOpts extends ShellExecOptions = ShellExecOptions>(
509
+ this: ADB,
510
+ cmd: string | string[],
511
+ opts?: TShellExecOpts
512
+ ): Promise<TShellExecOpts extends TFullOutputOption ? ExecResult : string> {
511
513
  const {
512
514
  privileged,
513
- } = opts ?? /** @type {TShellExecOpts} */ ({});
515
+ } = opts ?? {} as TShellExecOpts;
514
516
 
515
517
  const cmdArr = _.isArray(cmd) ? cmd : [cmd];
516
- const fullCmd = ['shell'];
518
+ const fullCmd: string[] = ['shell'];
517
519
  if (privileged) {
518
520
  log.info(`'adb shell ${util.quote(cmdArr)}' requires root access`);
519
521
  if (await this.isRoot()) {
@@ -529,12 +531,12 @@ export async function shell (cmd, opts) {
529
531
  }
530
532
 
531
533
  /**
534
+ * Create a new ADB subprocess with the given arguments
532
535
  *
533
- * @this {import('../adb.js').ADB}
534
- * @param {string[]} [args=[]]
535
- * @returns {import('teen_process').SubProcess}
536
+ * @param args - Array of command arguments (default: empty array)
537
+ * @returns A SubProcess instance
536
538
  */
537
- export function createSubProcess (args = []) {
539
+ export function createSubProcess (this: ADB, args: string[] = []): SubProcess {
538
540
  // add the default arguments
539
541
  const finalArgs = [...this.executable.defaultArgs, ...args];
540
542
  log.debug(`Creating ADB subprocess with args: ${JSON.stringify(finalArgs)}`);
@@ -544,69 +546,65 @@ export function createSubProcess (args = []) {
544
546
  /**
545
547
  * Retrieve the current adb port.
546
548
  * @todo can probably deprecate this now that the logic is just to read this.adbPort
549
+ * @deprecated Use this.adbPort instead
547
550
  *
548
- * @this {import('../adb.js').ADB}
549
- * @return {number} The current adb port number.
551
+ * @returns The ADB server port number
550
552
  */
551
- export function getAdbServerPort () {
552
- return /** @type {number} */ (this.adbPort);
553
+ export function getAdbServerPort (this: ADB): number {
554
+ return this.adbPort as number;
553
555
  }
554
556
 
555
557
  /**
556
- * Retrieve the current emulator port from _adb devives_ output.
558
+ * Retrieve the current emulator port from _adb devices_ output.
557
559
  *
558
- * @this {import('../adb.js').ADB}
559
- * @return {Promise<number>} The current emulator port.
560
- * @throws {Error} If there are no connected devices.
560
+ * @returns The emulator port number
561
+ * @throws {Error} If no devices are connected or emulator port cannot be found
561
562
  */
562
- export async function getEmulatorPort () {
563
+ export async function getEmulatorPort (this: ADB): Promise<number> {
563
564
  log.debug('Getting running emulator port');
564
- if (this.emulatorPort !== null) {
565
- return /** @type {number} */ (this.emulatorPort);
565
+ if (!_.isNil(this.emulatorPort)) {
566
+ return this.emulatorPort;
566
567
  }
567
568
  try {
568
- let devices = await this.getConnectedDevices();
569
- let port = this.getPortFromEmulatorString(devices[0].udid);
569
+ const devices = await this.getConnectedDevices();
570
+ const port = this.getPortFromEmulatorString(devices[0].udid);
570
571
  if (port) {
571
572
  return port;
572
573
  } else {
573
574
  throw new Error(`Emulator port not found`);
574
575
  }
575
- } catch (e) {
576
- throw new Error(`No devices connected. Original error: ${e.message}`);
576
+ } catch (e: unknown) {
577
+ const error = e as Error;
578
+ throw new Error(`No devices connected. Original error: ${error.message}`);
577
579
  }
578
580
  }
579
581
 
580
582
  /**
581
583
  * Retrieve the current emulator port by parsing emulator name string.
582
584
  *
583
- * @this {import('../adb.js').ADB}
584
- * @param {string} emStr - Emulator name string.
585
- * @return {number|false} Either the current emulator port or
586
- * _false_ if port number cannot be parsed.
585
+ * @param emStr - The emulator string (e.g., 'emulator-5554')
586
+ * @returns The port number if found, false otherwise
587
587
  */
588
- export function getPortFromEmulatorString (emStr) {
589
- let portPattern = /emulator-(\d+)/;
590
- if (portPattern.test(emStr)) {
591
- return parseInt((/** @type {RegExpExecArray} */(portPattern.exec(emStr)))[1], 10);
592
- }
593
- return false;
588
+ export function getPortFromEmulatorString (this: ADB, emStr: string): number | false {
589
+ const portPattern = /emulator-(\d+)/;
590
+ const match = portPattern.exec(emStr);
591
+ return match ? parseInt(match[1], 10) : false;
594
592
  }
595
593
 
596
594
  /**
597
595
  * Retrieve the list of currently connected emulators.
598
596
  *
599
- * @this {import('../adb.js').ADB}
600
- * @param {import('./types').ConnectedDevicesOptions} [opts={}] - Additional options mapping.
601
- * @return {Promise<import('./types').Device[]>} The list of connected devices.
597
+ * @param opts - Options for device retrieval
598
+ * @returns Array of connected emulator devices
599
+ * @throws {Error} If error occurs while getting emulators
602
600
  */
603
- export async function getConnectedEmulators (opts = {}) {
601
+ export async function getConnectedEmulators (this: ADB, opts: ConnectedDevicesOptions = {}): Promise<Device[]> {
604
602
  log.debug('Getting connected emulators');
605
603
  try {
606
- let devices = await this.getConnectedDevices(opts);
607
- let emulators = [];
608
- for (let device of devices) {
609
- let port = this.getPortFromEmulatorString(device.udid);
604
+ const devices = await this.getConnectedDevices(opts);
605
+ const emulators: Device[] = [];
606
+ for (const device of devices) {
607
+ const port = this.getPortFromEmulatorString(device.udid);
610
608
  if (port) {
611
609
  device.port = port;
612
610
  emulators.push(device);
@@ -614,31 +612,30 @@ export async function getConnectedEmulators (opts = {}) {
614
612
  }
615
613
  log.debug(`${util.pluralize('emulator', emulators.length, true)} connected`);
616
614
  return emulators;
617
- } catch (e) {
618
- throw new Error(`Error getting emulators. Original error: ${e.message}`);
615
+ } catch (e: unknown) {
616
+ const error = e as Error;
617
+ throw new Error(`Error getting emulators. Original error: ${error.message}`);
619
618
  }
620
619
  }
621
620
 
622
621
  /**
623
622
  * Set _emulatorPort_ property of the current class.
624
623
  *
625
- * @this {import('../adb.js').ADB}
626
- * @param {number} emPort - The emulator port to be set.
624
+ * @param emPort - The emulator port number
627
625
  */
628
- export function setEmulatorPort (emPort) {
626
+ export function setEmulatorPort (this: ADB, emPort: number): void {
629
627
  this.emulatorPort = emPort;
630
628
  }
631
629
 
632
630
  /**
633
631
  * Set the identifier of the current device (_this.curDeviceId_).
634
632
  *
635
- * @this {import('../adb.js').ADB}
636
- * @param {string} deviceId - The device identifier.
633
+ * @param deviceId - The device identifier
637
634
  */
638
- export function setDeviceId (deviceId) {
635
+ export function setDeviceId (this: ADB, deviceId: string): void {
639
636
  log.debug(`Setting device id to ${deviceId}`);
640
637
  this.curDeviceId = deviceId;
641
- let argsHasDevice = this.executable.defaultArgs.indexOf('-s');
638
+ const argsHasDevice = this.executable.defaultArgs.indexOf('-s');
642
639
  if (argsHasDevice !== -1) {
643
640
  // remove the old device id from the arguments
644
641
  this.executable.defaultArgs.splice(argsHasDevice, 2);
@@ -647,12 +644,11 @@ export function setDeviceId (deviceId) {
647
644
  }
648
645
 
649
646
  /**
650
- * Set the the current device object.
647
+ * Set the current device object.
651
648
  *
652
- * @this {import('../adb.js').ADB}
653
- * @param {import('./types').Device} deviceObj - The device object to be set.
649
+ * @param deviceObj - The device object containing udid and other properties
654
650
  */
655
- export function setDevice (deviceObj) {
651
+ export function setDevice (this: ADB, deviceObj: Device): void {
656
652
  const deviceId = deviceObj.udid;
657
653
  const emPort = this.getPortFromEmulatorString(deviceId);
658
654
  if (_.isNumber(emPort)) {
@@ -667,11 +663,11 @@ export function setDevice (deviceObj) {
667
663
  * `deviceId` (only if AVD with a matching name is found)
668
664
  * and `emulatorPort` instance properties.
669
665
  *
670
- * @this {import('../adb.js').ADB}
671
- * @param {string} avdName - Emulator name.
672
- * @return {Promise<import('./types').Device|null>} Currently running emulator or _null_.
666
+ * @param avdName - The name of the AVD to find
667
+ * @returns The device object if found, null otherwise
668
+ * @throws {Error} If error occurs while getting AVD
673
669
  */
674
- export async function getRunningAVD (avdName) {
670
+ export async function getRunningAVD (this: ADB, avdName: string): Promise<Device | null> {
675
671
  log.debug(`Trying to find '${avdName}' emulator`);
676
672
  try {
677
673
  const emulators = await this.getConnectedEmulators();
@@ -692,48 +688,48 @@ export async function getRunningAVD (avdName) {
692
688
  }
693
689
  log.debug(`Emulator '${avdName}' not running`);
694
690
  return null;
695
- } catch (e) {
696
- throw new Error(`Error getting AVD. Original error: ${e.message}`);
691
+ } catch (e: unknown) {
692
+ const error = e as Error;
693
+ throw new Error(`Error getting AVD. Original error: ${error.message}`);
697
694
  }
698
695
  }
699
696
 
700
697
  /**
701
- * Get the object for the currently running emulator.
698
+ * Get the object for the currently running emulator with retry.
702
699
  *
703
- * @this {import('../adb.js').ADB}
704
- * @param {string} avdName - Emulator name.
705
- * @param {number} [timeoutMs=20000] - The maximum number of milliseconds
706
- * to wait until at least one running AVD object
707
- * is detected.
708
- * @return {Promise<import('./types').Device|null>} Currently running emulator or _null_.
709
- * @throws {Error} If no device has been detected within the timeout.
700
+ * @param avdName - The name of the AVD to find
701
+ * @param timeoutMs - Maximum time to wait (default: 20000ms)
702
+ * @returns The device object if found, null otherwise
703
+ * @throws {Error} If error occurs while getting AVD with retry
710
704
  */
711
- export async function getRunningAVDWithRetry (avdName, timeoutMs = 20000) {
705
+ export async function getRunningAVDWithRetry (this: ADB, avdName: string, timeoutMs: number = 20000): Promise<Device | null> {
712
706
  try {
713
- return /** @type {import('./types').Device|null} */ (await waitForCondition(async () => {
707
+ return await waitForCondition(async () => {
714
708
  try {
715
709
  return await this.getRunningAVD(avdName.replace('@', ''));
716
- } catch (e) {
717
- log.debug(e.message);
710
+ } catch (e: unknown) {
711
+ const error = e as Error;
712
+ log.debug(error.message);
718
713
  return false;
719
714
  }
720
715
  }, {
721
716
  waitMs: timeoutMs,
722
717
  intervalMs: 1000,
723
- }));
724
- } catch (e) {
725
- throw new Error(`Error getting AVD with retry. Original error: ${e.message}`);
718
+ }) as Device | null;
719
+ } catch (e: unknown) {
720
+ const error = e as Error;
721
+ throw new Error(`Error getting AVD with retry. Original error: ${error.message}`);
726
722
  }
727
723
  }
728
724
 
729
725
  /**
730
726
  * Shutdown all running emulators by killing their processes.
731
727
  *
732
- * @this {import('../adb.js').ADB}
733
- * @throws {Error} If killing tool returned non-zero return code.
728
+ * @throws {Error} If error occurs while killing emulators
734
729
  */
735
- export async function killAllEmulators () {
736
- let cmd, args;
730
+ export async function killAllEmulators (this: ADB): Promise<void> {
731
+ let cmd: string;
732
+ let args: string[];
737
733
  if (system.isWindows()) {
738
734
  cmd = 'TASKKILL';
739
735
  args = ['TASKKILL', '/IM', 'emulator.exe'];
@@ -743,24 +739,22 @@ export async function killAllEmulators () {
743
739
  }
744
740
  try {
745
741
  await exec(cmd, args);
746
- } catch (e) {
747
- throw new Error(`Error killing emulators. Original error: ${e.message}`);
742
+ } catch (e: unknown) {
743
+ const error = e as Error;
744
+ throw new Error(`Error killing emulators. Original error: ${error.message}`);
748
745
  }
749
746
  }
750
747
 
751
748
  /**
752
749
  * Kill emulator with the given name. No error
753
- * is thrown is given avd does not exist/is not running.
750
+ * is thrown if given avd does not exist/is not running.
754
751
  *
755
- * @this {import('../adb.js').ADB}
756
- * @param {string?} [avdName=null] - The name of the emulator to be killed. If empty,
757
- * the current emulator will be killed.
758
- * @param {number} [timeout=60000] - The amount of time to wait before throwing
759
- * an exception about unsuccessful killing
760
- * @return {Promise<boolean>} - True if the emulator was killed, false otherwise.
761
- * @throws {Error} if there was a failure by killing the emulator
752
+ * @param avdName - The name of the AVD to kill (null to kill current AVD)
753
+ * @param timeout - Maximum time to wait for emulator to be killed (default: 60000ms)
754
+ * @returns True if emulator was killed, false if it was not running
755
+ * @throws {Error} If emulator is still running after timeout
762
756
  */
763
- export async function killEmulator (avdName = null, timeout = 60000) {
757
+ export async function killEmulator (this: ADB, avdName: string | null = null, timeout: number = 60000): Promise<boolean> {
764
758
  if (util.hasValue(avdName)) {
765
759
  log.debug(`Killing avd '${avdName}'`);
766
760
  const device = await this.getRunningAVD(avdName);
@@ -782,10 +776,11 @@ export async function killEmulator (avdName = null, timeout = 60000) {
782
776
  await waitForCondition(async () => {
783
777
  try {
784
778
  return util.hasValue(avdName)
785
- ? !await this.getRunningAVD(avdName)
779
+ ? !await this.getRunningAVD(avdName as string)
786
780
  : !await this.isEmulatorConnected();
787
- } catch {}
788
- return false;
781
+ } catch {
782
+ return false;
783
+ }
789
784
  }, {
790
785
  waitMs: timeout,
791
786
  intervalMs: 2000,
@@ -800,13 +795,12 @@ export async function killEmulator (avdName = null, timeout = 60000) {
800
795
  /**
801
796
  * Start an emulator with given parameters and wait until it is fully started.
802
797
  *
803
- * @this {import('../adb.js').ADB}
804
- * @param {string} avdName - The name of an existing emulator.
805
- * @param {import('./types').AvdLaunchOptions} [opts={}]
806
- * @returns {Promise<SubProcess>} Emulator subprocess instance
807
- * @throws {Error} If the emulator fails to start within the given timeout.
798
+ * @param avdName - The name of the AVD to launch
799
+ * @param opts - Launch options
800
+ * @returns The SubProcess instance for the launched emulator
801
+ * @throws {Error} If emulator fails to launch or boot
808
802
  */
809
- export async function launchAVD (avdName, opts = {}) {
803
+ export async function launchAVD (this: ADB, avdName: string, opts: AvdLaunchOptions = {}): Promise<SubProcess> {
810
804
  const {
811
805
  args = [],
812
806
  env = {},
@@ -819,13 +813,13 @@ export async function launchAVD (avdName, opts = {}) {
819
813
  log.debug(`Launching Emulator with AVD ${avdName}, launchTimeout ` +
820
814
  `${launchTimeout}ms and readyTimeout ${readyTimeout}ms`);
821
815
  const emulatorBinaryPath = await this.getSdkBinaryPath('emulator');
822
- if (avdName.startsWith('@')) {
823
- avdName = avdName.slice(1);
816
+ let processedAvdName = avdName;
817
+ if (processedAvdName.startsWith('@')) {
818
+ processedAvdName = processedAvdName.slice(1);
824
819
  }
825
- await this.checkAvdExist(avdName);
820
+ await this.checkAvdExist(processedAvdName);
826
821
 
827
- /** @type {string[]} */
828
- const launchArgs = ['-avd', avdName];
822
+ const launchArgs: string[] = ['-avd', processedAvdName];
829
823
  launchArgs.push(...(toAvdLocaleArgs(language ?? null, country ?? null)));
830
824
 
831
825
  let isDelayAdbFeatureEnabled = false;
@@ -834,7 +828,7 @@ export async function launchAVD (avdName, opts = {}) {
834
828
  if (revision && util.compareVersions(revision, '>=', '29.0.7')) {
835
829
  // https://androidstudio.googleblog.com/2019/05/emulator-2907-canary.html
836
830
  try {
837
- const {target} = await this.getEmuImageProperties(avdName);
831
+ const {target} = await this.getEmuImageProperties(processedAvdName);
838
832
  const apiMatch = /\d+/.exec(target);
839
833
  // https://issuetracker.google.com/issues/142533355
840
834
  if (apiMatch && parseInt(apiMatch[0], 10) >= MIN_DELAY_ADB_API_LEVEL) {
@@ -843,9 +837,10 @@ export async function launchAVD (avdName, opts = {}) {
843
837
  } else {
844
838
  throw new Error(`The actual image API version is below ${MIN_DELAY_ADB_API_LEVEL}`);
845
839
  }
846
- } catch (e) {
840
+ } catch (e: unknown) {
841
+ const error = e as Error;
847
842
  log.info(`The -delay-adb emulator startup detection feature will not be enabled. ` +
848
- `Original error: ${e.message}`);
843
+ `Original error: ${error.message}`);
849
844
  }
850
845
  }
851
846
  } else {
@@ -853,7 +848,7 @@ export async function launchAVD (avdName, opts = {}) {
853
848
  }
854
849
 
855
850
  if (!_.isEmpty(args)) {
856
- launchArgs.push(...(_.isArray(args) ? args : /** @type {string[]} */ (util.shellParse(`${args}`))));
851
+ launchArgs.push(...(_.isArray(args) ? args : util.shellParse(`${args}`)));
857
852
  }
858
853
 
859
854
  log.debug(`Running '${emulatorBinaryPath}' with args: ${util.quote(launchArgs)}`);
@@ -865,19 +860,20 @@ export async function launchAVD (avdName, opts = {}) {
865
860
  });
866
861
  await proc.start(0);
867
862
  for (const streamName of ['stderr', 'stdout']) {
868
- proc.on(`line-${streamName}`, (line) => log.debug(`[AVD OUTPUT] ${line}`));
863
+ proc.on(`line-${streamName}`, (line: string) => log.debug(`[AVD OUTPUT] ${line}`));
869
864
  }
870
- proc.on('die', (code, signal) => {
871
- log.warn(`Emulator avd ${avdName} exited with code ${code}${signal ? `, signal ${signal}` : ''}`);
865
+ proc.on('die', (code: number | null, signal: string | null) => {
866
+ log.warn(`Emulator avd ${processedAvdName} exited with code ${code}${signal ? `, signal ${signal}` : ''}`);
872
867
  });
873
- await retry(retryTimes, async () => await this.getRunningAVDWithRetry(avdName, launchTimeout));
868
+ await retry(retryTimes, async () => await this.getRunningAVDWithRetry(processedAvdName, launchTimeout));
874
869
  // At this point we have deviceId already assigned
875
870
  const timer = new timing.Timer().start();
876
871
  if (isDelayAdbFeatureEnabled) {
877
872
  try {
878
873
  await this.adbExec(['wait-for-device'], {timeout: readyTimeout});
879
- } catch (e) {
880
- throw new Error(`'${avdName}' Emulator has failed to boot: ${e.stderr || e.message}`);
874
+ } catch (e: unknown) {
875
+ const error = e as ExecError;
876
+ throw new Error(`'${processedAvdName}' Emulator has failed to boot: ${error.stderr || error.message}`);
881
877
  }
882
878
  }
883
879
  await this.waitForEmulatorReady(Math.trunc(readyTimeout - timer.getDuration().asMilliSeconds));
@@ -887,48 +883,45 @@ export async function launchAVD (avdName, opts = {}) {
887
883
  /**
888
884
  * Get the adb version. The result of this method is cached.
889
885
  *
890
- * @this {import('../adb.js').ADB}
891
- * @return {Promise<import('./types').Version>}
892
- * @throws {Error} If it is not possible to parse adb binary version.
886
+ * @returns Version information object
887
+ * @throws {Error} If error occurs while getting adb version
893
888
  */
894
- export const getVersion = _.memoize(async function getVersion () {
895
- let stdout;
889
+ export const getVersion = _.memoize(async function getVersion (this: ADB): Promise<Version> {
890
+ let stdout: string;
896
891
  try {
897
892
  stdout = await this.adbExec('version');
898
- } catch (e) {
899
- throw new Error(`Error getting adb version: ${e.stderr || e.message}`);
893
+ } catch (e: unknown) {
894
+ const error = e as ExecError;
895
+ throw new Error(`Error getting adb version: ${error.stderr || error.message}`);
900
896
  }
901
897
 
902
- const result = {};
898
+ const result: Partial<Version> = {};
903
899
  const binaryVersionMatch = BINARY_VERSION_PATTERN.exec(stdout);
904
900
  if (binaryVersionMatch) {
905
901
  result.binary = {
906
- version: semver.coerce(binaryVersionMatch[1]),
902
+ version: semver.coerce(binaryVersionMatch[1])?.version || binaryVersionMatch[1],
907
903
  build: parseInt(binaryVersionMatch[2], 10),
908
904
  };
909
905
  }
910
906
  const bridgeVersionMatch = BRIDGE_VERSION_PATTERN.exec(stdout);
911
907
  if (bridgeVersionMatch) {
912
908
  result.bridge = {
913
- version: semver.coerce(bridgeVersionMatch[1]),
909
+ version: semver.coerce(bridgeVersionMatch[1])?.version || bridgeVersionMatch[1],
914
910
  };
915
911
  }
916
- return result;
912
+ return result as Version;
917
913
  });
918
914
 
919
915
  /**
920
916
  * Check if the current emulator is ready to accept further commands (booting completed).
921
917
  *
922
- * @this {import('../adb.js').ADB}
923
- * @param {number} [timeoutMs=20000] - The maximum number of milliseconds to wait.
924
- * @returns {Promise<void>}
925
- * @throws {Error} If the emulator is not ready within the given timeout.
918
+ * @param timeoutMs - Maximum time to wait (default: 20000ms)
919
+ * @throws {Error} If emulator is not ready within the timeout period
926
920
  */
927
- export async function waitForEmulatorReady (timeoutMs = 20000) {
921
+ export async function waitForEmulatorReady (this: ADB, timeoutMs: number = 20000): Promise<void> {
928
922
  log.debug(`Waiting up to ${timeoutMs}ms for the emulator to be ready`);
929
- /** @type {RegExp[]} */
930
923
  const requiredServicesRe = REQUIRED_SERVICES.map((name) => new RegExp(`\\b${name}:`));
931
- let services;
924
+ let services: string | undefined;
932
925
  const timer = new timing.Timer().start();
933
926
  let isFirstCheck = true;
934
927
  let isBootCompleted = false;
@@ -958,15 +951,17 @@ export async function waitForEmulatorReady (timeoutMs = 20000) {
958
951
  isBootCompleted = true;
959
952
  }
960
953
 
961
- services = await this.shell(['service', 'list']);
962
- if (!requiredServicesRe.every((pattern) => pattern.test(services))) {
963
- log.debug(`Running services: ${services}`);
954
+ const servicesOutput = await this.shell(['service', 'list']);
955
+ services = servicesOutput;
956
+ if (!servicesOutput || !requiredServicesRe.every((pattern) => pattern.test(servicesOutput))) {
957
+ log.debug(`Running services: ${servicesOutput}`);
964
958
  return false;
965
959
  }
966
960
 
967
961
  return true;
968
- } catch (err) {
969
- log.debug(`Intermediate error: ${err.message}`);
962
+ } catch (err: unknown) {
963
+ const error = err as Error;
964
+ log.debug(`Intermediate error: ${error.message}`);
970
965
  return false;
971
966
  }
972
967
  }, {
@@ -975,9 +970,10 @@ export async function waitForEmulatorReady (timeoutMs = 20000) {
975
970
  });
976
971
  } catch {
977
972
  let suffix = '';
978
- if (services !== undefined) {
973
+ const servicesValue = services;
974
+ if (servicesValue) {
979
975
  const missingServices = _.zip(REQUIRED_SERVICES, requiredServicesRe)
980
- .filter(([, pattern]) => !(/** @type {RegExp} */ (pattern)).test(services))
976
+ .filter(([, pattern]) => !(pattern as RegExp).test(servicesValue))
981
977
  .map(([name]) => name);
982
978
  suffix = ` (${missingServices} service${missingServices.length === 1 ? ' is' : 's are'} not running)`;
983
979
  }
@@ -993,14 +989,12 @@ export async function waitForEmulatorReady (timeoutMs = 20000) {
993
989
  /**
994
990
  * Check if the current device is ready to accept further commands (booting completed).
995
991
  *
996
- * @this {import('../adb.js').ADB}
997
- * @param {number} [appDeviceReadyTimeout=30] - The maximum number of seconds to wait.
998
- * @throws {Error} If the device is not ready within the given timeout.
992
+ * @param appDeviceReadyTimeout - Timeout in seconds (default: 30)
993
+ * @throws {Error} If device is not ready within the timeout period
999
994
  */
1000
- export async function waitForDevice (appDeviceReadyTimeout = 30) {
995
+ export async function waitForDevice (this: ADB, appDeviceReadyTimeout: number = 30): Promise<void> {
1001
996
  const timeoutMs = appDeviceReadyTimeout * 1000;
1002
- /** @type {Error|null} */
1003
- let lastError = null;
997
+ let lastErrorMessage: string | null = null;
1004
998
  try {
1005
999
  await waitForCondition(
1006
1000
  async () => {
@@ -1008,8 +1002,9 @@ export async function waitForDevice (appDeviceReadyTimeout = 30) {
1008
1002
  await this.adbExec('wait-for-device', {timeout: Math.trunc(timeoutMs * 0.99)});
1009
1003
  await this.ping();
1010
1004
  return true;
1011
- } catch (e) {
1012
- lastError = e;
1005
+ } catch (e: unknown) {
1006
+ const error = e as Error;
1007
+ lastErrorMessage = error.message;
1013
1008
  try {
1014
1009
  try {
1015
1010
  await this.reconnect();
@@ -1017,7 +1012,9 @@ export async function waitForDevice (appDeviceReadyTimeout = 30) {
1017
1012
  await this.restartAdb();
1018
1013
  }
1019
1014
  await this.getConnectedDevices();
1020
- } catch {}
1015
+ } catch {
1016
+ // Ignore errors during reconnection
1017
+ }
1021
1018
  return false;
1022
1019
  }
1023
1020
  }, {
@@ -1027,8 +1024,8 @@ export async function waitForDevice (appDeviceReadyTimeout = 30) {
1027
1024
  );
1028
1025
  } catch {
1029
1026
  let suffix = '';
1030
- if (lastError) {
1031
- suffix = ` Original error: ${ /** @type {Error} */ (lastError).message}`;
1027
+ if (lastErrorMessage) {
1028
+ suffix = ` Original error: ${lastErrorMessage}`;
1032
1029
  }
1033
1030
  throw new Error(`The device is not ready after ${appDeviceReadyTimeout}s.${suffix}`);
1034
1031
  }
@@ -1037,11 +1034,10 @@ export async function waitForDevice (appDeviceReadyTimeout = 30) {
1037
1034
  /**
1038
1035
  * Reboot the current device and wait until it is completed.
1039
1036
  *
1040
- * @this {import('../adb.js').ADB}
1041
- * @param {number} [retries=DEFAULT_ADB_REBOOT_RETRIES] - The maximum number of reboot retries.
1042
- * @throws {Error} If the device failed to reboot and number of retries is exceeded.
1037
+ * @param retries - Number of retry attempts (default: 90)
1038
+ * @throws {Error} If reboot fails or device is not ready after reboot
1043
1039
  */
1044
- export async function reboot (retries = DEFAULT_ADB_REBOOT_RETRIES) {
1040
+ export async function reboot (this: ADB, retries: number = DEFAULT_ADB_REBOOT_RETRIES): Promise<void> {
1045
1041
  // Get root access so we can run the next shell commands which require root access
1046
1042
  const { wasAlreadyRooted } = await this.root();
1047
1043
  try {
@@ -1052,15 +1048,16 @@ export async function reboot (retries = DEFAULT_ADB_REBOOT_RETRIES) {
1052
1048
  privileged: false // no need to set privileged true because device already rooted
1053
1049
  });
1054
1050
  await this.shell(['start']);
1055
- } catch (e) {
1056
- const {message} = e;
1051
+ } catch (e: unknown) {
1052
+ const error = e as Error;
1053
+ const {message} = error;
1057
1054
 
1058
1055
  // provide a helpful error message if the reason reboot failed was because ADB couldn't gain root access
1059
1056
  if (message.includes('must be root')) {
1060
1057
  throw new Error(`Could not reboot device. Rebooting requires root access and ` +
1061
1058
  `attempt to get root access on device failed with error: '${message}'`);
1062
1059
  }
1063
- throw e;
1060
+ throw error;
1064
1061
  } finally {
1065
1062
  // Return root state to what it was before
1066
1063
  if (!wasAlreadyRooted) {
@@ -1082,21 +1079,21 @@ export async function reboot (retries = DEFAULT_ADB_REBOOT_RETRIES) {
1082
1079
  /**
1083
1080
  * Switch adb server root privileges.
1084
1081
  *
1085
- * @this {import('../adb.js').ADB}
1086
- * @param {boolean} isElevated - Should we elevate to to root or unroot? (default true)
1087
- * @return {Promise<import('./types').RootResult>}
1082
+ * @param isElevated - True to enable root, false to disable
1083
+ * @returns Result object indicating success and whether device was already rooted
1088
1084
  */
1089
- export async function changeUserPrivileges (isElevated) {
1085
+ export async function changeUserPrivileges (this: ADB, isElevated: boolean): Promise<RootResult> {
1090
1086
  const cmd = isElevated ? 'root' : 'unroot';
1091
1087
 
1092
- const retryIfOffline = async (cmdFunc) => {
1088
+ const retryIfOffline = async (cmdFunc: () => Promise<any>): Promise<any> => {
1093
1089
  try {
1094
1090
  return await cmdFunc();
1095
- } catch (err) {
1091
+ } catch (err: unknown) {
1092
+ const error = err as ExecError;
1096
1093
  // Check the output of the stdErr to see if there's any clues that show that the device went offline
1097
1094
  // and if it did go offline, restart ADB
1098
1095
  if (['closed', 'device offline', 'timeout expired']
1099
- .some((x) => (err.stderr || '').toLowerCase().includes(x))) {
1096
+ .some((x) => (error.stderr || '').toLowerCase().includes(x))) {
1100
1097
  log.warn(`Attempt to ${cmd} caused ADB to think the device went offline`);
1101
1098
  try {
1102
1099
  await this.reconnect();
@@ -1105,7 +1102,7 @@ export async function changeUserPrivileges (isElevated) {
1105
1102
  }
1106
1103
  return await cmdFunc();
1107
1104
  } else {
1108
- throw err;
1105
+ throw error;
1109
1106
  }
1110
1107
  }
1111
1108
  };
@@ -1132,8 +1129,9 @@ export async function changeUserPrivileges (isElevated) {
1132
1129
  }
1133
1130
  }
1134
1131
  return {isSuccessful: true, wasAlreadyRooted};
1135
- } catch (err) {
1136
- const {stderr = '', message} = err;
1132
+ } catch (err: unknown) {
1133
+ const error = err as ExecError;
1134
+ const {stderr = '', message} = error;
1137
1135
  log.warn(`Unable to ${cmd} adb daemon. Original error: '${message}'. Stderr: '${stderr}'. Continuing.`);
1138
1136
  return {isSuccessful: false, wasAlreadyRooted};
1139
1137
  }
@@ -1142,32 +1140,27 @@ export async function changeUserPrivileges (isElevated) {
1142
1140
  /**
1143
1141
  * Switch adb server to root mode
1144
1142
  *
1145
- * @this {import('../adb.js').ADB}
1146
- * @return {Promise<import('./types').RootResult>}
1143
+ * @returns Result object indicating success and whether device was already rooted
1147
1144
  */
1148
- export async function root () {
1145
+ export async function root (this: ADB): Promise<RootResult> {
1149
1146
  return await this.changeUserPrivileges(true);
1150
1147
  }
1151
1148
 
1152
1149
  /**
1153
1150
  * Switch adb server to non-root mode.
1154
1151
  *
1155
- * @this {import('../adb.js').ADB}
1156
- * @return {Promise<import('./types').RootResult>}
1152
+ * @returns Result object indicating success and whether device was already rooted
1157
1153
  */
1158
- export async function unroot () {
1154
+ export async function unroot (this: ADB): Promise<RootResult> {
1159
1155
  return await this.changeUserPrivileges(false);
1160
1156
  }
1161
1157
 
1162
1158
  /**
1163
1159
  * Checks whether the current user is root
1164
1160
  *
1165
- * @this {import('../adb.js').ADB}
1166
- * @return {Promise<boolean>} True if the user is root
1167
- * @throws {Error} if there was an error while identifying
1168
- * the user.
1161
+ * @returns True if current user is root, false otherwise
1169
1162
  */
1170
- export async function isRoot () {
1163
+ export async function isRoot (this: ADB): Promise<boolean> {
1171
1164
  return (await this.shell(['whoami'])).trim() === 'root';
1172
1165
  }
1173
1166
 
@@ -1180,13 +1173,10 @@ export async function isRoot () {
1180
1173
  * Read https://github.com/appium/appium/issues/10964
1181
1174
  * for more details on this topic
1182
1175
  *
1183
- * @this {import('../adb.js').ADB}
1184
- * @param {Buffer|string} cert - base64-decoded content of the actual certificate
1185
- * represented as a string or a buffer
1186
- * @throws {Error} If openssl tool is not available on the destination system
1187
- * or if there was an error while installing the certificate
1176
+ * @param cert - Certificate as Buffer or base64-encoded string
1177
+ * @throws {Error} If certificate installation fails
1188
1178
  */
1189
- export async function installMitmCertificate (cert) {
1179
+ export async function installMitmCertificate (this: ADB, cert: Buffer | string): Promise<void> {
1190
1180
  const openSsl = await getOpenSslForOs();
1191
1181
 
1192
1182
  const tmpRoot = await tempDir.openDir();
@@ -1215,11 +1205,12 @@ export async function installMitmCertificate (cert) {
1215
1205
  await this.push(dstCert, CERTS_ROOT);
1216
1206
  log.debug('Remounting /system to confirm changes');
1217
1207
  await this.adbExec(['remount']);
1218
- } catch (err) {
1208
+ } catch (err: unknown) {
1209
+ const error = err as Error;
1219
1210
  throw new Error(`Cannot inject the custom certificate. ` +
1220
1211
  `Is the certificate properly encoded into base64-string? ` +
1221
1212
  `Do you have root permissions on the device? ` +
1222
- `Original error: ${err.message}`);
1213
+ `Original error: ${error.message}`);
1223
1214
  } finally {
1224
1215
  await fs.rimraf(tmpRoot);
1225
1216
  }
@@ -1228,27 +1219,25 @@ export async function installMitmCertificate (cert) {
1228
1219
  /**
1229
1220
  * Verifies if the given root certificate is already installed on the device.
1230
1221
  *
1231
- * @this {import('../adb.js').ADB}
1232
- * @param {Buffer|string} cert - base64-decoded content of the actual certificate
1233
- * represented as a string or a buffer
1234
- * @throws {Error} If openssl tool is not available on the destination system
1235
- * or if there was an error while checking the certificate
1236
- * @returns {Promise<boolean>} true if the given certificate is already installed
1222
+ * @param cert - Certificate as Buffer or base64-encoded string
1223
+ * @returns True if certificate is installed, false otherwise
1224
+ * @throws {Error} If certificate hash cannot be retrieved
1237
1225
  */
1238
- export async function isMitmCertificateInstalled (cert) {
1226
+ export async function isMitmCertificateInstalled (this: ADB, cert: Buffer | string): Promise<boolean> {
1239
1227
  const openSsl = await getOpenSslForOs();
1240
1228
 
1241
1229
  const tmpRoot = await tempDir.openDir();
1242
- let certHash;
1230
+ let certHash: string;
1243
1231
  try {
1244
1232
  const tmpCert = path.resolve(tmpRoot, 'source.cer');
1245
1233
  await fs.writeFile(tmpCert, Buffer.isBuffer(cert) ? cert : Buffer.from(cert, 'base64'));
1246
1234
  const {stdout} = await exec(openSsl, ['x509', '-noout', '-hash', '-in', tmpCert]);
1247
1235
  certHash = stdout.trim();
1248
- } catch (err) {
1236
+ } catch (err: unknown) {
1237
+ const error = err as Error;
1249
1238
  throw new Error(`Cannot retrieve the certificate hash. ` +
1250
1239
  `Is the certificate properly encoded into base64-string? ` +
1251
- `Original error: ${err.message}`);
1240
+ `Original error: ${error.message}`);
1252
1241
  } finally {
1253
1242
  await fs.rimraf(tmpRoot);
1254
1243
  }
@@ -1263,19 +1252,13 @@ export async function isMitmCertificateInstalled (cert) {
1263
1252
  * there is a limit for a maximum length of a single adb command. that is why
1264
1253
  * we need all this complicated logic.
1265
1254
  *
1266
- * @this {import('../adb.js').ADB}
1267
- * @param {(x: string) => string[]} argTransformer A function, that receives single argument
1268
- * from the `args` array and transforms it into a shell command. The result
1269
- * of the function must be an array, where each item is a part of a single command.
1270
- * The last item of the array could be ';'. If this is not a semicolon then it is going to
1271
- * be added automatically.
1272
- * @param {string[]} args Array of argument values to create chunks for
1273
- * @throws {Error} If any of the chunks returns non-zero exit code after being executed
1255
+ * @param argTransformer - Function to transform each argument into command array
1256
+ * @param args - Array of arguments to process
1257
+ * @throws {Error} If argument transformer returns invalid result or command execution fails
1274
1258
  */
1275
- export async function shellChunks (argTransformer, args) {
1276
- const commands = [];
1277
- /** @type {string[]} */
1278
- let cmdChunk = [];
1259
+ export async function shellChunks (this: ADB, argTransformer: (x: string) => string[], args: string[]): Promise<void> {
1260
+ const commands: string[][] = [];
1261
+ let cmdChunk: string[] = [];
1279
1262
  for (const arg of args) {
1280
1263
  const nextCmd = argTransformer(arg);
1281
1264
  if (!_.isArray(nextCmd)) {
@@ -1294,12 +1277,12 @@ export async function shellChunks (argTransformer, args) {
1294
1277
  commands.push(cmdChunk);
1295
1278
  }
1296
1279
  log.debug(`Got the following command chunks to execute: ${JSON.stringify(commands)}`);
1297
- let lastError = null;
1280
+ let lastError: Error | null = null;
1298
1281
  for (const cmd of commands) {
1299
1282
  try {
1300
1283
  await this.shell(cmd);
1301
- } catch (e) {
1302
- lastError = e;
1284
+ } catch (e: unknown) {
1285
+ lastError = e as Error;
1303
1286
  }
1304
1287
  }
1305
1288
  if (lastError) {
@@ -1307,26 +1290,23 @@ export async function shellChunks (argTransformer, args) {
1307
1290
  }
1308
1291
  }
1309
1292
 
1310
- // #region Private functions
1311
-
1312
1293
  /**
1313
1294
  * Transforms the given language and country abbreviations
1314
1295
  * to AVD arguments array
1315
1296
  *
1316
- * @param {?string} language Language name, for example 'fr'
1317
- * @param {?string} country Country name, for example 'CA'
1318
- * @returns {Array<string>} The generated arguments. The
1319
- * resulting array might be empty if both arguments are empty
1297
+ * @param language - Language code (e.g., 'en', 'fr')
1298
+ * @param country - Country code (e.g., 'US', 'FR')
1299
+ * @returns Array of AVD locale arguments
1320
1300
  */
1321
- export function toAvdLocaleArgs (language, country) {
1322
- const result = [];
1301
+ export function toAvdLocaleArgs (language: string | null, country: string | null): string[] {
1302
+ const result: string[] = [];
1323
1303
  if (language && _.isString(language)) {
1324
1304
  result.push('-prop', `persist.sys.language=${language.toLowerCase()}`);
1325
1305
  }
1326
1306
  if (country && _.isString(country)) {
1327
1307
  result.push('-prop', `persist.sys.country=${country.toUpperCase()}`);
1328
1308
  }
1329
- let locale;
1309
+ let locale: string | undefined;
1330
1310
  if (_.isString(language) && _.isString(country) && language && country) {
1331
1311
  locale = language.toLowerCase() + '-' + country.toUpperCase();
1332
1312
  } else if (language && _.isString(language)) {
@@ -1340,52 +1320,37 @@ export function toAvdLocaleArgs (language, country) {
1340
1320
  return result;
1341
1321
  }
1342
1322
 
1343
-
1344
1323
  /**
1345
1324
  * Retrieves full paths to all 'build-tools' subfolders under the particular
1346
1325
  * SDK root folder
1347
1326
  *
1348
- * @type {(sdkRoot: string) => Promise<string[]>}
1327
+ * @param sdkRoot - The Android SDK root directory path
1328
+ * @returns Array of build-tools directory paths (newest first)
1349
1329
  */
1350
- export const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot) {
1330
+ export const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot: string): Promise<string[]> {
1351
1331
  let buildToolsDirs = await fs.glob('*/', {
1352
1332
  cwd: path.resolve(sdkRoot, 'build-tools'),
1353
1333
  absolute: true,
1354
1334
  });
1355
1335
  try {
1356
1336
  buildToolsDirs = buildToolsDirs
1357
- .map((dir) => [path.basename(dir), dir])
1337
+ .map((dir) => [path.basename(dir), dir] as [string, string])
1358
1338
  .sort((a, b) => semver.rcompare(a[0], b[0]))
1359
1339
  .map((pair) => pair[1]);
1360
- } catch (err) {
1340
+ } catch (err: unknown) {
1341
+ const error = err as Error;
1361
1342
  log.warn(`Cannot sort build-tools folders ${JSON.stringify(buildToolsDirs.map((dir) => path.basename(dir)))} ` +
1362
1343
  `by semantic version names.`);
1363
- log.warn(`Falling back to sorting by modification date. Original error: ${err.message}`);
1364
- /** @type {[number, string][]} */
1365
- const pairs = await B.map(buildToolsDirs, async (dir) => [(await fs.stat(dir)).mtime.valueOf(), dir]);
1344
+ log.warn(`Falling back to sorting by modification date. Original error: ${error.message}`);
1345
+ const pairs = await B.map(buildToolsDirs, async (dir) => [(await fs.stat(dir)).mtime.valueOf(), dir] as [number, string]);
1366
1346
  buildToolsDirs = pairs
1367
- // @ts-ignore This sorting works
1368
- .sort((a, b) => a[0] < b[0])
1347
+ .sort((a, b) => a[0] < b[0] ? 1 : -1)
1369
1348
  .map((pair) => pair[1]);
1370
1349
  }
1371
1350
  log.info(`Found ${buildToolsDirs.length} 'build-tools' folders under '${sdkRoot}' (newest first):`);
1372
- for (let dir of buildToolsDirs) {
1351
+ for (const dir of buildToolsDirs) {
1373
1352
  log.info(` ${dir}`);
1374
1353
  }
1375
1354
  return buildToolsDirs;
1376
1355
  });
1377
1356
 
1378
- /**
1379
- *
1380
- * @returns {Promise<string>}
1381
- */
1382
- async function getOpenSslForOs () {
1383
- const binaryName = `openssl${system.isWindows() ? '.exe' : ''}`;
1384
- try {
1385
- return await fs.which(binaryName);
1386
- } catch {
1387
- throw new Error('The openssl tool must be installed on the system and available on the path');
1388
- }
1389
- }
1390
-
1391
- // #endregion