Skip to content

Commit

Permalink
feat!: Improve type declarations (#698)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Some type declarations have been changed in order to make the compiler happy
  • Loading branch information
mykola-mokhnach authored Oct 16, 2023
1 parent badadf9 commit 7d2588a
Show file tree
Hide file tree
Showing 21 changed files with 836 additions and 452 deletions.
12 changes: 12 additions & 0 deletions lib/adb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import methods, {getAndroidBinaryPath} from './tools';
import {DEFAULT_ADB_EXEC_TIMEOUT, requireSdkRoot, getSdkRootFromEnv} from './helpers';
import log from './logger';
import type {ADBOptions, ADBExecutable} from './options';
import type { LogcatOpts } from './logcat';
import type { LRUCache } from 'lru-cache';

const DEFAULT_ADB_PORT = 5037;
export const DEFAULT_OPTS = {
Expand All @@ -22,6 +24,16 @@ export const DEFAULT_OPTS = {
export class ADB {
adbHost?: string;
adbPort?: number;
_apiLevel: number|undefined;
_logcatStartupParams: LogcatOpts|undefined;
_doesPsSupportAOption: boolean|undefined;
_isPgrepAvailable: boolean|undefined;
_canPgrepUseFullCmdLineSearch: boolean|undefined;
_isPidofAvailable: boolean|undefined;
_memoizedFeatures: (() => Promise<string>)|undefined;
_areExtendedLsOptionsSupported: boolean|undefined;
remoteAppsCache: LRUCache<string, string>|undefined;
_isLockManagementSupported: boolean|undefined;

executable: ADBExecutable;
constructor(opts: Partial<ADBOptions> = {}) {
Expand Down
97 changes: 55 additions & 42 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,34 +82,36 @@ async function requireSdkRoot (customRoot = null) {
throw new Error(`Neither ANDROID_HOME nor ANDROID_SDK_ROOT environment variable was exported. ${docMsg}`);
}

if (!await fs.exists(sdkRoot)) {
if (!await fs.exists(/** @type {string} */ (sdkRoot))) {
throw new Error(`The Android SDK root folder '${sdkRoot}' does not exist on the local file system. ${docMsg}`);
}
const stats = await fs.stat(sdkRoot);
const stats = await fs.stat(/** @type {string} */ (sdkRoot));
if (!stats.isDirectory()) {
throw new Error(`The Android SDK root '${sdkRoot}' must be a folder. ${docMsg}`);
}
return sdkRoot;
return /** @type {string} */ (sdkRoot);
}

/**
* @typedef {Object} PlatformInfo
* @property {?string} platform - The platform name, for example `android-24`
* @property {string?} platform - The platform name, for example `android-24`
* or `null` if it cannot be found
* @property {?string} platformPath - Full path to the platform SDK folder
* @property {string?} platformPath - Full path to the platform SDK folder
* or `null` if it cannot be found
*/

/**
* Retrieve the path to the recent installed Android platform.
*
* @return {PlatformInfo} The resulting path to the newest installed platform.
* @param {string} sdkRoot
* @return {Promise<PlatformInfo>} The resulting path to the newest installed platform.
*/
async function getAndroidPlatformAndPath (sdkRoot) {
const propsPaths = await fs.glob('*/build.prop', {
cwd: path.resolve(sdkRoot, 'platforms'),
absolute: true,
});
/** @type {Record<string, PlatformInfo>} */
const platformsMapping = {};
for (const propsPath of propsPaths) {
const propsContent = await fs.readFile(propsPath, 'utf-8');
Expand Down Expand Up @@ -140,6 +142,10 @@ async function getAndroidPlatformAndPath (sdkRoot) {
return result;
}

/**
* @param {string} zipPath
* @param {string} dstRoot
*/
async function unzipFile (zipPath, dstRoot = path.dirname(zipPath)) {
log.debug(`Unzipping '${zipPath}' to '${dstRoot}'`);
await zip.assertValidZip(zipPath);
Expand All @@ -153,7 +159,7 @@ async function unzipFile (zipPath, dstRoot = path.dirname(zipPath)) {
* !!! The function overwrites the given apk after successful unsigning !!!
*
* @param {string} apkPath The path to the apk
* @returns {boolean} `true` if the apk has been successfully
* @returns {Promise<boolean>} `true` if the apk has been successfully
* unsigned and overwritten
* @throws {Error} if there was an error during the unsign operation
*/
Expand Down Expand Up @@ -185,6 +191,10 @@ async function unsignApk (apkPath) {
}
}

/**
* @param {string} stdout
* @returns {string[]}
*/
function getIMEListFromOutput (stdout) {
let engines = [];
for (let line of stdout.split('\n')) {
Expand All @@ -196,6 +206,7 @@ function getIMEListFromOutput (stdout) {
return engines;
}

/** @type {() => Promise<string>} */
const getJavaHome = _.memoize(async function getJavaHome () {
const result = process.env.JAVA_HOME;
if (!result) {
Expand All @@ -211,6 +222,7 @@ const getJavaHome = _.memoize(async function getJavaHome () {
return result;
});

/** @type {() => Promise<string>} */
const getJavaForOs = _.memoize(async function getJavaForOs () {
let javaHome;
let errMsg;
Expand All @@ -230,9 +242,10 @@ const getJavaForOs = _.memoize(async function getJavaForOs () {
return await fs.which(executableName);
} catch (ign) {}
throw new Error(`The '${executableName}' binary could not be found ` +
`neither in PATH nor under JAVA_HOME (${errMsg || path.resolve(javaHome, 'bin')})`);
`neither in PATH nor under JAVA_HOME (${javaHome ? path.resolve(javaHome, 'bin') : errMsg})`);
});

/** @type {() => Promise<string>} */
const getOpenSslForOs = async function () {
const binaryName = `openssl${system.isWindows() ? '.exe' : ''}`;
try {
Expand All @@ -258,7 +271,7 @@ async function getApksignerForOs (sysHelpers) {
* https://developer.android.com/studio/command-line/apkanalyzer.html
*
* @param {Object} sysHelpers - An instance containing systemCallMethods helper methods
* @returns {string} An absolute path to apkanalyzer tool.
* @returns {Promise<string>} An absolute path to apkanalyzer tool.
* @throws {Error} If the tool is not present on the local file system.
*/
async function getApkanalyzerForOs (sysHelpers) {
Expand Down Expand Up @@ -310,16 +323,16 @@ function getSurfaceOrientation (dumpsys) {
function isScreenOnFully (dumpsys) {
let m = /mScreenOnFully=\w+/gi.exec(dumpsys);
return !m || // if information is missing we assume screen is fully on
(m && m.length > 0 && m[0].split('=')[1] === 'true') || false;
(m && m.length > 0 && m[0].split('=')[1] === 'true') || false;
}

/**
* Builds command line representation for the given
* application startup options
*
* @param {StartAppOptions} startAppOptions - Application options mapping
* @param {Record<string, any>} startAppOptions - Application options mapping
* @param {number} apiLevel - The actual OS API level
* @returns {Array<String>} The actual command line array
* @returns {string[]} The actual command line array
*/
function buildStartCmd (startAppOptions, apiLevel) {
const {
Expand Down Expand Up @@ -408,6 +421,7 @@ function buildStartCmd (startAppOptions, apiLevel) {
return cmd;
}

/** @type {() => Promise<{major: number, minor: number, build: number}?>} */
const getSdkToolsVersion = _.memoize(async function getSdkToolsVersion () {
const androidHome = process.env.ANDROID_HOME;
if (!androidHome) {
Expand All @@ -416,7 +430,7 @@ const getSdkToolsVersion = _.memoize(async function getSdkToolsVersion () {
const propertiesPath = path.resolve(androidHome, 'tools', 'source.properties');
if (!await fs.exists(propertiesPath)) {
log.warn(`Cannot find ${propertiesPath} file to read SDK version from`);
return;
return null;
}
const propertiesContent = await fs.readFile(propertiesPath, 'utf8');
const versionMatcher = new RegExp(/Pkg\.Revision=(\d+)\.?(\d+)?\.?(\d+)?/);
Expand All @@ -429,15 +443,14 @@ const getSdkToolsVersion = _.memoize(async function getSdkToolsVersion () {
};
}
log.warn(`Cannot parse "Pkg.Revision" value from ${propertiesPath}`);
return null;
});

/**
* Retrieves full paths to all 'build-tools' subfolders under the particular
* SDK root folder
*
* @param {string} sdkRoot - The full path to the Android SDK root folder
* @returns {Array<string>} The full paths to the resulting folders sorted by
* modification date (the newest comes first) or an empty list if no macthes were found
* @type {(sdkRoot: string) => Promise<string[]>}
*/
const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot) {
let buildToolsDirs = await fs.glob('*/', {
Expand All @@ -453,8 +466,10 @@ const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot) {
log.warn(`Cannot sort build-tools folders ${JSON.stringify(buildToolsDirs.map((dir) => path.basename(dir)))} ` +
`by semantic version names.`);
log.warn(`Falling back to sorting by modification date. Original error: ${err.message}`);
/** @type {[number, string][]} */
const pairs = await B.map(buildToolsDirs, async (dir) => [(await fs.stat(dir)).mtime.valueOf(), dir]);
buildToolsDirs = pairs
// @ts-ignore This sorting works
.sort((a, b) => a[0] < b[0])
.map((pair) => pair[1]);
}
Expand All @@ -469,10 +484,10 @@ const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot) {
* Retrieves the list of permission names encoded in `dumpsys package` command output.
*
* @param {string} dumpsysOutput - The actual command output.
* @param {Array<string>} groupNames - The list of group names to list permissions for.
* @param {?boolean} grantedState - The expected state of `granted` attribute to filter with.
* No filtering is done if the parameter is not set.
* @returns {Array<string>} The list of matched permission names or an empty list if no matches were found.
* @param {string[]} groupNames - The list of group names to list permissions for.
* @param {boolean?} [grantedState=null] - The expected state of `granted` attribute to filter with.
* No filtering is done if the parameter is not set.
* @returns {string[]} The list of matched permission names or an empty list if no matches were found.
*/
const extractMatchingPermissions = function (dumpsysOutput, groupNames, grantedState = null) {
const groupPatternByName = (groupName) => new RegExp(`^(\\s*${_.escapeRegExp(groupName)} permissions:[\\s\\S]+)`, 'm');
Expand Down Expand Up @@ -523,27 +538,27 @@ const extractMatchingPermissions = function (dumpsysOutput, groupNames, grantedS

/**
* @typedef {Object} InstallOptions
* @property {boolean} allowTestPackages [false] - Set to true in order to allow test
* @property {boolean} [allowTestPackages=false] - Set to true in order to allow test
* packages installation.
* @property {boolean} useSdcard [false] - Set to true to install the app on sdcard
* @property {boolean} [useSdcard=false] - Set to true to install the app on sdcard
* instead of the device memory.
* @property {boolean} grantPermissions [false] - Set to true in order to grant all the
* @property {boolean} [grantPermissions=false] - Set to true in order to grant all the
* permissions requested in the application's manifest
* automatically after the installation is completed
* under Android 6+.
* @property {boolean} replace [true] - Set it to false if you don't want
* @property {boolean} [replace=true] - Set it to false if you don't want
* the application to be upgraded/reinstalled
* if it is already present on the device.
* @property {boolean} partialInstall [false] - Install apks partially. It is used for 'install-multiple'.
* @property {boolean} [partialInstall=false] - Install apks partially. It is used for 'install-multiple'.
* https://android.stackexchange.com/questions/111064/what-is-a-partial-application-install-via-adb
*/

/**
* Transforms given options into the list of `adb install.install-multiple` command arguments
*
* @param {number} apiLevel - The current API level
* @param {?InstallOptions} options - The options mapping to transform
* @returns {Array<String>} The array of arguments
* @param {InstallOptions} [options={}] - The options mapping to transform
* @returns {string[]} The array of arguments
*/
function buildInstallArgs (apiLevel, options = {}) {
const result = [];
Expand Down Expand Up @@ -577,7 +592,7 @@ function buildInstallArgs (apiLevel, options = {}) {
/**
* @typedef {Object} ManifestInfo
* @property {string} pkg - The application identifier
* @property {string} activity - The name of the main package activity
* @property {string} [activity] - The name of the main package activity
* @property {?number} versionCode - The version code number (might be `NaN`)
* @property {?string} versionName - The version name (might be `null`)
*/
Expand All @@ -586,7 +601,7 @@ function buildInstallArgs (apiLevel, options = {}) {
* Perform parsing of the manifest object in order
* to extract some vital values from it
*
* @param {object} manifest The manifest content formatted as JSON
* @param {Record<string, any>} manifest The manifest content formatted as JSON
* See https://www.npmjs.com/package/adbkit-apkreader for detailed format description
* @returns {ManifestInfo}
*/
Expand Down Expand Up @@ -829,9 +844,9 @@ function parseAapt2Strings (rawOutput, configMarker) {
*
* @param {Function} configsGetter The function whose result is a list
* of apk configs
* @param {string} desiredMarker The desired config marker value
* @param {string?} desiredMarker The desired config marker value
* @param {string} defaultMarker The default config marker value
* @return {string} The formatted config marker
* @return {Promise<string>} The formatted config marker
*/
async function formatConfigMarker (configsGetter, desiredMarker, defaultMarker) {
let configMarker = desiredMarker || defaultMarker;
Expand Down Expand Up @@ -925,12 +940,12 @@ function toAvdLocaleArgs (language, country) {
/**
* Retrieves the full path to the Android preferences root
*
* @returns {?string} The full path to the folder or `null` if the folder cannot be found
* @returns {Promise<string?>} The full path to the folder or `null` if the folder cannot be found
*/
async function getAndroidPrefsRoot () {
let location = process.env.ANDROID_EMULATOR_HOME;
if (await dirExists(location)) {
return location;
if (await dirExists(location ?? '')) {
return location ?? null;
}

if (location) {
Expand All @@ -942,24 +957,22 @@ async function getAndroidPrefsRoot () {
location = path.resolve(home, '.android');
}

if (!await dirExists(location)) {
if (!await dirExists(location ?? '')) {
log.debug(`Android config root '${location}' is not an existing directory`);
return null;
}

return location;
return location ?? null;
}

/**
* Check if a path exists on the filesystem and is a directory
*
* @param {?string} location The full path to the directory
* @returns {boolean}
* @param {string} location The full path to the directory
* @returns {Promise<boolean>}
*/
async function dirExists (location) {
return location
&& await fs.exists(location)
&& (await fs.stat(location)).isDirectory();
return await fs.exists(location) && (await fs.stat(location)).isDirectory();
}

/**
Expand Down Expand Up @@ -1067,7 +1080,7 @@ function parseLaunchableActivityNames (dumpsys) {
* Check if the given string is a valid component name
*
* @param {string} classString The string to verify
* @return {?Array<Match>} The result of Regexp.exec operation
* @return {RegExpExecArray?} The result of Regexp.exec operation
* or _null_ if no matches are found
*/
function matchComponentName (classString) {
Expand Down
21 changes: 21 additions & 0 deletions lib/logcat.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ const DEFAULT_PRIORITY = 'v';
const DEFAULT_TAG = '*';
const DEFAULT_FORMAT = 'threadtime';

/**
* @typedef {Object} LogcatOpts
* @property {string} [format] The log print format, where <format> is one of:
* brief process tag thread raw time threadtime long
* `threadtime` is the default value.
* @property {Array<string>} [filterSpecs] Series of `<tag>[:priority]`
* where `<tag>` is a log component tag (or `*` for all) and priority is:
* V Verbose
* D Debug
* I Info
* W Warn
* E Error
* F Fatal
* S Silent (supress all output)
*
* `'*'` means `'*:d'` and `<tag>` by itself means `<tag>:v`
*
* If not specified on the commandline, filterspec is set from `ANDROID_LOG_TAGS`.
* If no filterspec is found, filter defaults to `'*:I'`
*/

function requireFormat (format) {
if (!SUPPORTED_FORMATS.includes(format)) {
log.info(`The format value '${format}' is unknown. Supported values are: ${SUPPORTED_FORMATS}`);
Expand Down
2 changes: 2 additions & 0 deletions lib/mixins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
KeyboardCommands,
ApkSigningCommands,
ApksUtils,
AabUtils,
} from './tools';
import {ADBOptions} from './options';

declare module './adb' {
// note that ADBOptions is the options object, but it's mixed directly in to the instance in the constructor.
interface ADB
extends ADBCommands,
AabUtils,
ApkUtils,
ApksUtils,
SystemCalls,
Expand Down
3 changes: 2 additions & 1 deletion lib/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SubProcess } from 'teen_process';
import type Logcat from './logcat';
import type {StringRecord} from '@appium/types';

Expand All @@ -16,7 +17,7 @@ export interface ADBOptions {
emulatorPort?: number;
logcat?: Logcat;
binaries?: StringRecord;
instrumentProc?: string;
instrumentProc?: SubProcess;
suppressKillServer?: boolean;
jars?: StringRecord;
adbPort?: number;
Expand Down
1 change: 1 addition & 0 deletions lib/stubs.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
declare module 'adbkit-apkreader';
declare module 'asyncbox';
Loading

0 comments on commit 7d2588a

Please sign in to comment.