appium-adb 14.0.3 → 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.
- package/CHANGELOG.md +6 -0
- package/build/lib/adb.d.ts +3 -11
- package/build/lib/adb.d.ts.map +1 -1
- package/build/lib/helpers.d.ts +29 -67
- package/build/lib/helpers.d.ts.map +1 -1
- package/build/lib/helpers.js +65 -98
- package/build/lib/helpers.js.map +1 -1
- package/build/lib/logger.d.ts +1 -1
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/tools/system-calls.d.ts +157 -267
- package/build/lib/tools/system-calls.d.ts.map +1 -1
- package/build/lib/tools/system-calls.js +259 -312
- package/build/lib/tools/system-calls.js.map +1 -1
- package/lib/{helpers.js → helpers.ts} +113 -117
- package/lib/{logger.js → logger.ts} +1 -0
- package/lib/tools/{system-calls.js → system-calls.ts} +372 -407
- package/package.json +3 -3
|
@@ -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 {
|
|
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
|
|
51
|
-
* @
|
|
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
|
-
* @
|
|
75
|
-
* @
|
|
76
|
-
* @
|
|
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 (
|
|
86
|
-
return
|
|
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
|
-
|
|
90
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
146
|
-
*
|
|
147
|
-
* @
|
|
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
|
-
* @
|
|
173
|
-
* @
|
|
174
|
-
* @
|
|
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 (
|
|
180
|
-
return
|
|
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
|
-
|
|
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
|
-
* @
|
|
199
|
-
* @
|
|
200
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
261
|
-
* @
|
|
262
|
-
*
|
|
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
|
-
|
|
279
|
-
log.
|
|
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
|
|
293
|
-
} catch (e) {
|
|
294
|
-
|
|
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
|
-
* @
|
|
306
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
388
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
408
|
-
* @
|
|
409
|
-
* @
|
|
410
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
463
|
-
return
|
|
464
|
+
if (error.code === 0 && error.stdout) {
|
|
465
|
+
return error.stdout.replace(LINKER_WARNING_REGEXP, '').trim();
|
|
464
466
|
}
|
|
465
467
|
|
|
466
|
-
if (_.isNull(
|
|
467
|
-
|
|
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
|
-
|
|
472
|
-
`Command output: ${
|
|
473
|
+
error.message = `Error executing adbExec. Original error: '${error.message}'; ` +
|
|
474
|
+
`Command output: ${error.stderr || error.stdout || '<empty>'}`;
|
|
473
475
|
}
|
|
474
|
-
throw
|
|
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
|
-
* @
|
|
502
|
-
* @
|
|
503
|
-
* @
|
|
504
|
-
*
|
|
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
|
|
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 ??
|
|
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
|
-
* @
|
|
534
|
-
* @
|
|
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
|
-
* @
|
|
549
|
-
* @return {number} The current adb port number.
|
|
551
|
+
* @returns The ADB server port number
|
|
550
552
|
*/
|
|
551
|
-
export function getAdbServerPort () {
|
|
552
|
-
return
|
|
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
|
|
558
|
+
* Retrieve the current emulator port from _adb devices_ output.
|
|
557
559
|
*
|
|
558
|
-
* @
|
|
559
|
-
* @
|
|
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
|
|
565
|
-
return
|
|
565
|
+
if (!_.isNil(this.emulatorPort)) {
|
|
566
|
+
return this.emulatorPort;
|
|
566
567
|
}
|
|
567
568
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
584
|
-
* @
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
* @
|
|
600
|
-
* @
|
|
601
|
-
* @
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
for (
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
|
647
|
+
* Set the current device object.
|
|
651
648
|
*
|
|
652
|
-
* @
|
|
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
|
-
* @
|
|
671
|
-
* @
|
|
672
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
704
|
-
* @param
|
|
705
|
-
* @
|
|
706
|
-
*
|
|
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
|
|
707
|
+
return await waitForCondition(async () => {
|
|
714
708
|
try {
|
|
715
709
|
return await this.getRunningAVD(avdName.replace('@', ''));
|
|
716
|
-
} catch (e) {
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
|
750
|
+
* is thrown if given avd does not exist/is not running.
|
|
754
751
|
*
|
|
755
|
-
* @
|
|
756
|
-
* @param
|
|
757
|
-
*
|
|
758
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
804
|
-
* @param
|
|
805
|
-
* @
|
|
806
|
-
* @
|
|
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
|
-
|
|
823
|
-
|
|
816
|
+
let processedAvdName = avdName;
|
|
817
|
+
if (processedAvdName.startsWith('@')) {
|
|
818
|
+
processedAvdName = processedAvdName.slice(1);
|
|
824
819
|
}
|
|
825
|
-
await this.checkAvdExist(
|
|
820
|
+
await this.checkAvdExist(processedAvdName);
|
|
826
821
|
|
|
827
|
-
|
|
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(
|
|
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: ${
|
|
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 :
|
|
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 ${
|
|
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(
|
|
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
|
-
|
|
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
|
-
* @
|
|
891
|
-
* @
|
|
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
|
-
|
|
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
|
-
* @
|
|
923
|
-
* @
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
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
|
-
|
|
973
|
+
const servicesValue = services;
|
|
974
|
+
if (servicesValue) {
|
|
979
975
|
const missingServices = _.zip(REQUIRED_SERVICES, requiredServicesRe)
|
|
980
|
-
.filter(([, pattern]) => !(
|
|
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
|
-
* @
|
|
997
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1031
|
-
suffix = ` Original error: ${
|
|
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
|
-
* @
|
|
1041
|
-
* @
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
1086
|
-
* @
|
|
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) => (
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
1184
|
-
* @
|
|
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: ${
|
|
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
|
-
* @
|
|
1232
|
-
* @
|
|
1233
|
-
*
|
|
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: ${
|
|
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
|
-
* @
|
|
1267
|
-
* @param
|
|
1268
|
-
*
|
|
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
|
-
|
|
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
|
|
1317
|
-
* @param
|
|
1318
|
-
* @returns
|
|
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
|
-
* @
|
|
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: ${
|
|
1364
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|