Android emulator selection for launch scenarios (#1361)

* create androidEmulatorManager

* fix path to commandExecutor

* init LaunchScenariosManager

* add selection for android emulator

* polishing, fixing and add command for launch emulator

* refactor resolveEmulator logic, add telemetry step and create abstract EmulatorManager class

* move logic of updating launch scenraio to LaunchScenarioManager

* added the ability to use device id

* add unit tests for GeneralMobilePlatform

* add unit tests for LaunchScenarioManager

* rename EmulatorManager to VirtualDeviceManager

* add emulator launch command to readme

* update terminateAndroidEmulator function

Co-authored-by: Yuri Skorokhodov <v-yuskor@microsoft.com>
Co-authored-by: RedMickey <33267199+RedMickey@users.noreply.github.com>
Co-authored-by: JiglioNero <admin@sierra.local>
This commit is contained in:
JiglioNero 2020-08-19 12:40:19 +03:00 коммит произвёл GitHub
Родитель b78a1aae0d
Коммит 68a5b8d5c6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 698 добавлений и 91 удалений

Просмотреть файл

@ -99,6 +99,7 @@ The full list of commands is:
|Name|Description|
|---|---|
|Launch Android Emulator|Prompts you to select the name of the available emulator and launch it. If only one emulator is installed in the system, it will be selected automatically|
|Run Android on Emulator|Run an Android application on Emulator. Launch order: check target platform support, load run arguments, start Packager, run app in all connected emulators|
|Run Android on Device|Run an Android application on Device. Launch order: check target platform support, load run arguments, start Packager, run app in all connected devices|
|Run iOS on Simulator|Run an iOS application on Simulator. Launch order: load run arguments, check target platform support, start Packager, run app in only one connected emulator|
@ -308,7 +309,7 @@ The following is a list of all the configuration properties the debugger accepts
|`skipFiles`|An array of file or folder names, or glob patterns, to skip when debugging|`array`|`[]`|
|`debuggerWorkerUrlPath`|Path to the app debugger worker to override. For example, if debugger tries to attach to http://localhost:8081/debugger-ui/debuggerWorker.js and you get 404 error from packager output then you may want to change debuggerWorkerUrlPath to another value suitable for your packager (\"debugger-ui\" will be replaced with the value you provide)|`string`|`debugger-ui/`|
|`platform`|The platform to target. Possible values: `android`, `ios`, `exponent`, `windows`, `wpf`|`string`|n/a|
|`target`|Target to run on. Possible values: `simulator`, `device`, `device=<iOS device name>`, [`<Android emulator/device id>`](https://github.com/react-native-community/cli/blob/master/docs/commands.md#--deviceid-string), `<iOS simulator name>`|`string`|`simulator`|
|`target`|Target to run on. Possible values: `simulator`, `device`, `device=<iOS device name>`, [`<Android emulator/device id>`](https://github.com/react-native-community/cli/blob/master/docs/commands.md#--deviceid-string), `<Android emulator name>`, `<iOS simulator name>`. If the value is `simulator` then the quick pick window will be expanded with the names of the available virtual devices, then the target value in `launch.json` will be changed to the name of the selected virtual device. If you have only one virtual device available, it will be selected automatically.|`string`|`simulator`|
|`logCatArguments`|Arguments to be used for LogCat (The LogCat output will appear on an Output Channel). It can be an array such as: `[":S", "ReactNative:V", "ReactNativeJS:V"]`|`array`|`["*:S", "ReactNative:V", "ReactNativeJS:V"]`|
|`runArguments`|Run arguments to be passed to `react-native run-<platform>` command (override all other configuration params)|`array`|n/a|
|`launchActivity`|The Android activity to be launched for debugging, e.g. it specifies [`--main-activity`](https://github.com/react-native-community/cli/blob/master/docs/commands.md#--main-activity-string) parameter in `react-native` run arguments|`string`|`MainActivity`|

Просмотреть файл

@ -295,8 +295,7 @@ gulp.task("clean", () => {
"!test/resources/sampleReactNative022Project/**/*.js",
".vscode-test/",
"nls.*.json",
"!test/smoke/**/*.js",
"!test/smoke/**/*.js.map",
"!test/smoke/**/*",
]
return del(pathsToDelete, { force: true });
});

Просмотреть файл

@ -49,6 +49,11 @@
"main": "./src/extension/rn-extension",
"contributes": {
"commands": [
{
"command": "reactNative.launchAndroidSimulator-preview",
"title": "%reactNative.command.launchAndroidSimulator.title%",
"category": "React Native (Preview)"
},
{
"command": "reactNative.runAndroidSimulator-preview",
"title": "%reactNative.command.runAndroidSimulator.title%",

Просмотреть файл

@ -1,6 +1,7 @@
{
"reactNative.description":"Debugging and integrated commands for React Native",
"reactNative.license":"SEE LICENSE IN LICENSE.txt",
"reactNative.command.launchAndroidSimulator.title":"Launch Android Emulator",
"reactNative.command.runAndroidSimulator.title":"Run Android on Emulator",
"reactNative.command.runAndroidDevice.title":"Run Android on Device",
"reactNative.command.runIosSimulator.title":"Run iOS on Simulator",

Просмотреть файл

@ -78,4 +78,5 @@ export const ERROR_STRINGS = {
[InternalErrorCode.CouldntImportScriptAt]: localize("CouldntImportScriptAt", "Couldn't import script at <{0}>"),
[InternalErrorCode.RNMessageWithMethodExecuteApplicationScriptDoesntHaveURLProperty]: localize("RNMessageWithMethodExecuteApplicationScriptDoesntHaveURLProperty", "RNMessage with method 'executeApplicationScript' doesn't have 'url' property"),
[InternalErrorCode.CouldNotConnectToDebugTarget]: localize("CouldNotConnectToDebugTarget", "Could not connect to the debug target at {0}: {1}"),
[InternalErrorCode.FailedToStartAndroidEmulator]: localize("FailedToStartAndroidEmulator", "The command \"emulator -avd {0}\" threw an exception: {1}"),
};

Просмотреть файл

@ -24,6 +24,7 @@ export enum InternalErrorCode {
DeveloperDiskImgNotMountable = 302,
ApplicationLaunchFailed = 303,
ApplicationLaunchTimedOut = 304,
FailedToStartAndroidEmulator = 305,
// iOS Platform errors
IOSSimulatorNotLaunchable = 401,

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as nls from "vscode-nls";
import { QuickPickOptions, window } from "vscode";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
export interface IVirtualDevice {
name?: string;
id: string;
}
export abstract class VirtualDeviceManager {
protected async selectVirtualDevice(filter?: (el: IVirtualDevice) => {}): Promise<string | undefined> {
const emulatorsList = await this.getVirtualDevicesNamesList(filter);
const quickPickOptions: QuickPickOptions = {
ignoreFocusOut: true,
canPickMany: false,
placeHolder: localize("SelectVirtualDevice", "Select virtual device for launch application"),
};
let result: string | undefined = emulatorsList[0];
if (emulatorsList.length > 1) {
result = await window.showQuickPick(emulatorsList, quickPickOptions);
}
return result?.toString();
}
public abstract async startSelection(): Promise<string | undefined>;
protected abstract async getVirtualDevicesNamesList(filter?: (el: IVirtualDevice) => {}): Promise<string[]>;
}

Просмотреть файл

@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import { AdbHelper } from "./adb";
import { ChildProcess } from "../../common/node/childProcess";
import { IVirtualDevice, VirtualDeviceManager } from "../VirtualDeviceManager";
import { OutputChannelLogger } from "../log/OutputChannelLogger";
import * as nls from "vscode-nls";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
export interface IAndroidEmulator extends IVirtualDevice {
}
export class AndroidEmulatorManager extends VirtualDeviceManager {
private static readonly EMULATOR_COMMAND = "emulator";
private static readonly EMULATOR_LIST_AVDS_COMMAND = `-list-avds`;
private static readonly EMULATOR_AVD_START_COMMAND = `-avd`;
private static readonly EMULATOR_START_TIMEOUT = 120;
private logger: OutputChannelLogger = OutputChannelLogger.getChannel(OutputChannelLogger.MAIN_CHANNEL_NAME, true);
private adbHelper: AdbHelper;
private childProcess: ChildProcess;
constructor(adbHelper: AdbHelper) {
super();
this.adbHelper = adbHelper;
this.childProcess = new ChildProcess();
}
public async startEmulator(target: string): Promise<IAndroidEmulator | null> {
const onlineDevices = await this.adbHelper.getOnlineDevices();
for (let i = 0; i < onlineDevices.length; i++){
if (onlineDevices[i].id === target) {
return {id: onlineDevices[i].id};
}
}
if (target && (await this.adbHelper.getOnlineDevices()).length === 0) {
if (target === "simulator") {
const newEmulator = await this.startSelection();
if (newEmulator) {
const emulatorId = await this.tryLaunchEmulatorByName(newEmulator);
return {name: newEmulator, id: emulatorId};
}
}
else if (!target.includes("device")) {
const emulatorId = await this.tryLaunchEmulatorByName(target);
return {name: target, id: emulatorId};
}
}
return null;
}
public async tryLaunchEmulatorByName(emulatorName: string): Promise<string> {
return new Promise((resolve, reject) => {
const emulatorProcess = this.childProcess.spawn(AndroidEmulatorManager.EMULATOR_COMMAND, [AndroidEmulatorManager.EMULATOR_AVD_START_COMMAND, emulatorName], {
detached: true,
});
emulatorProcess.outcome.catch((error) => {
reject(error);
});
emulatorProcess.spawnedProcess.unref();
const rejectTimeout = setTimeout(() => {
cleanup();
reject(`Could not start the emulator within ${AndroidEmulatorManager.EMULATOR_START_TIMEOUT} seconds.`);
}, AndroidEmulatorManager.EMULATOR_START_TIMEOUT * 1000);
const bootCheckInterval = setInterval(async () => {
const connectedDevices = await this.adbHelper.getOnlineDevices();
if (connectedDevices.length > 0) {
this.logger.info(localize("EmulatorLaunched", "Launched emulator {0}", emulatorName));
cleanup();
resolve(connectedDevices[0].id);
}
}, 1000);
const cleanup = () => {
clearTimeout(rejectTimeout);
clearInterval(bootCheckInterval);
};
});
}
public startSelection(): Promise<string | undefined> {
return this.selectVirtualDevice();
}
protected async getVirtualDevicesNamesList(): Promise<string[]> {
const res = await this.childProcess.execToString(`${AndroidEmulatorManager.EMULATOR_COMMAND} ${AndroidEmulatorManager.EMULATOR_LIST_AVDS_COMMAND}`);
let emulatorsList: string[] = [];
if (res) {
emulatorsList = res.split(/\r?\n|\r/g);
const indexOfBlank = emulatorsList.indexOf("");
if (emulatorsList.indexOf("") >= 0) {
emulatorsList.splice(indexOfBlank, 1);
}
}
return emulatorsList;
}
}

Просмотреть файл

@ -17,6 +17,7 @@ import { InternalErrorCode } from "../../common/error/internalErrorCode";
import { ErrorHelper } from "../../common/error/errorHelper";
import { isNullOrUndefined } from "util";
import { PromiseUtil } from "../../common/node/promise";
import { AndroidEmulatorManager, IAndroidEmulator } from "./androidEmulatorManager";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
@ -51,6 +52,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
private packageName: string;
private logCatMonitor: LogCatMonitor | null = null;
private adbHelper: AdbHelper;
private emulatorManager: AndroidEmulatorManager;
private needsToLaunchApps: boolean = false;
@ -66,6 +68,7 @@ export class AndroidPlatform extends GeneralMobilePlatform {
constructor(protected runOptions: IAndroidRunOptions, platformDeps: MobilePlatformDeps = {}) {
super(runOptions, platformDeps);
this.adbHelper = new AdbHelper(this.runOptions.projectRoot, this.logger);
this.emulatorManager = new AndroidEmulatorManager(this.adbHelper);
}
// TODO: remove this method when sinon will be updated to upper version. Now it is used for tests only.
@ -73,6 +76,21 @@ export class AndroidPlatform extends GeneralMobilePlatform {
this.adbHelper = adbHelper;
}
public resolveVirtualDevice(target: string): Promise<IAndroidEmulator | null> {
if (!target.includes("device")) {
return this.emulatorManager.startEmulator(target)
.then((emulator: IAndroidEmulator | null) => {
if (emulator) {
GeneralMobilePlatform.setRunArgument(this.runArguments, "--deviceId", emulator.id);
}
return emulator;
});
}
else {
return Promise.resolve(null);
}
}
public runApp(shouldLaunchInAllDevices: boolean = false): Promise<void> {
let extProps: any = {
platform: {

Просмотреть файл

@ -11,7 +11,7 @@ import {PackagerStatusIndicator} from "./packagerStatusIndicator";
import {CommandExecutor} from "../common/commandExecutor";
import {isNullOrUndefined} from "../common/utils";
import {OutputChannelLogger} from "./log/OutputChannelLogger";
import {MobilePlatformDeps} from "./generalMobilePlatform";
import {MobilePlatformDeps, GeneralMobilePlatform} from "./generalMobilePlatform";
import {PlatformResolver} from "./platformResolver";
import {ProjectVersionHelper} from "../common/projectVersionHelper";
import {TelemetryHelper} from "../common/telemetryHelper";
@ -25,6 +25,8 @@ import {generateRandomPortNumber} from "../common/extensionHelper";
import {DEBUG_TYPES} from "./debugConfigurationProvider";
import * as nls from "vscode-nls";
import { MultipleLifetimesAppWorker } from "../debugger/appWorker";
import { LaunchScenariosManager } from "./launchScenariosManager";
import { IVirtualDevice } from "./VirtualDeviceManager";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
@ -41,6 +43,7 @@ export class AppLauncher {
private rnCdpProxy: ReactNativeCDPProxy;
private logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
private logCatMonitor: LogCatMonitor | null = null;
private launchScenariosManager: LaunchScenariosManager;
public static getAppLauncherByProjectRootPath(projectRootPath: string): AppLauncher {
const appLauncher = ProjectsStorage.projectsCache[projectRootPath.toLowerCase()];
@ -57,6 +60,7 @@ export class AppLauncher {
this.cdpProxyHostAddress = "127.0.0.1"; // localhost
const rootPath = workspaceFolder.uri.fsPath;
this.launchScenariosManager = new LaunchScenariosManager(rootPath);
const projectRootPath = SettingsHelper.getReactNativeProjectRoot(rootPath);
this.exponentHelper = new ExponentHelper(rootPath, projectRootPath);
const packagerStatusIndicator: PackagerStatusIndicator = new PackagerStatusIndicator(rootPath);
@ -208,55 +212,55 @@ export class AppLauncher {
TelemetryHelper.generate("launch", extProps, (generator) => {
generator.step("checkPlatformCompatibility");
TargetPlatformHelper.checkTargetPlatformSupport(mobilePlatformOptions.platform);
return mobilePlatform.beforeStartPackager()
.then(() => {
generator.step("startPackager");
return mobilePlatform.startPackager();
})
.then(() => {
// We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
// and the user needs to Reload JS manually. We prewarm it to prevent that issue
generator.step("prewarmBundleCache");
this.logger.info(localize("PrewarmingBundleCache", "Prewarming bundle cache. This may take a while ..."));
return mobilePlatform.prewarmBundleCache();
})
.then(() => {
generator.step("mobilePlatform.runApp").add("target", mobilePlatformOptions.target, false);
this.logger.info(localize("BuildingAndRunningApplication", "Building and running application."));
return mobilePlatform.runApp();
})
.then(() => {
if (mobilePlatformOptions.isDirect || !mobilePlatformOptions.enableDebug) {
if (mobilePlatformOptions.isDirect && launchArgs.platform === "android") {
generator.step("mobilePlatform.enableDirectDebuggingMode");
if (mobilePlatformOptions.enableDebug) {
this.logger.info(localize("PrepareHermesDebugging", "Prepare Hermes debugging (experimental)"));
} else {
this.logger.info(localize("PrepareHermesLaunch", "Prepare Hermes launch (experimental)"));
}
generator.step("resolveEmulator");
return this.resolveAndSaveVirtualDevice(mobilePlatform, launchArgs, mobilePlatformOptions)
.then(() => mobilePlatform.beforeStartPackager())
.then(() => {
generator.step("startPackager");
return mobilePlatform.startPackager();
})
.then(() => {
// We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
// and the user needs to Reload JS manually. We prewarm it to prevent that issue
generator.step("prewarmBundleCache");
this.logger.info(localize("PrewarmingBundleCache", "Prewarming bundle cache. This may take a while ..."));
return mobilePlatform.prewarmBundleCache();
})
.then(() => {
generator.step("mobilePlatform.runApp").add("target", mobilePlatformOptions.target, false);
this.logger.info(localize("BuildingAndRunningApplication", "Building and running application."));
return mobilePlatform.runApp();
})
.then(() => {
if (mobilePlatformOptions.isDirect || !mobilePlatformOptions.enableDebug) {
if (mobilePlatformOptions.isDirect && launchArgs.platform === "android") {
generator.step("mobilePlatform.enableDirectDebuggingMode");
if (mobilePlatformOptions.enableDebug) {
this.logger.info(localize("PrepareHermesDebugging", "Prepare Hermes debugging (experimental)"));
} else {
generator.step("mobilePlatform.disableJSDebuggingMode");
this.logger.info(localize("DisableJSDebugging", "Disable JS Debugging"));
this.logger.info(localize("PrepareHermesLaunch", "Prepare Hermes launch (experimental)"));
}
return mobilePlatform.disableJSDebuggingMode();
} else {
generator.step("mobilePlatform.disableJSDebuggingMode");
this.logger.info(localize("DisableJSDebugging", "Disable JS Debugging"));
}
generator.step("mobilePlatform.enableJSDebuggingMode");
this.logger.info(localize("EnableJSDebugging", "Enable JS Debugging"));
return mobilePlatform.enableJSDebuggingMode();
})
.then(() => {
resolve();
})
.catch(error => {
if (!mobilePlatformOptions.enableDebug && launchArgs.platform === "ios") {
// If we disable debugging mode for iOS scenarios, we'll we ignore the error and run the 'run-ios' command anyway,
// since the error doesn't affects an application launch process
return resolve();
}
generator.addError(error);
this.logger.error(error);
reject(error);
});
return mobilePlatform.disableJSDebuggingMode();
}
generator.step("mobilePlatform.enableJSDebuggingMode");
this.logger.info(localize("EnableJSDebugging", "Enable JS Debugging"));
return mobilePlatform.enableJSDebuggingMode();
})
.then(resolve)
.catch(error => {
if (!mobilePlatformOptions.enableDebug && launchArgs.platform === "ios") {
// If we disable debugging mode for iOS scenarios, we'll we ignore the error and run the 'run-ios' command anyway,
// since the error doesn't affects an application launch process
return resolve();
}
generator.addError(error);
this.logger.error(error);
reject(error);
});
});
})
.catch(error => {
@ -279,6 +283,25 @@ export class AppLauncher {
});
}
private resolveAndSaveVirtualDevice(mobilePlatform: GeneralMobilePlatform, launchArgs: any, mobilePlatformOptions: any): Promise<void> {
if (launchArgs.target && mobilePlatformOptions.platform === "android") {
return mobilePlatform.resolveVirtualDevice(launchArgs.target)
.then((emulator: IVirtualDevice | null) => {
if (emulator && emulator.name) {
this.launchScenariosManager.updateLaunchScenario(launchArgs, {target: emulator.name});
if (launchArgs.platform === "android") {
mobilePlatformOptions.target = emulator.id;
}
}
else if (!emulator && mobilePlatformOptions.target.indexOf("device") < 0) {
mobilePlatformOptions.target = "simulator";
mobilePlatform.runArguments = mobilePlatform.getRunArguments();
}
});
}
return Promise.resolve();
}
private requestSetup(args: any): any {
const workspaceFolder: vscode.WorkspaceFolder = <vscode.WorkspaceFolder>vscode.workspace.getWorkspaceFolder(vscode.Uri.file(args.cwd || args.program));
const projectRootPath = this.getProjectRoot(args);

Просмотреть файл

@ -22,6 +22,8 @@ import * as nls from "vscode-nls";
import {ErrorHelper} from "../common/error/errorHelper";
import {InternalErrorCode} from "../common/error/internalErrorCode";
import {AppLauncher} from "./appLauncher";
import { AndroidEmulatorManager } from "./android/androidEmulatorManager";
import { AdbHelper } from "./android/adb";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
@ -99,6 +101,16 @@ export class CommandPaletteHandler {
});
}
public static async launchAndroidEmulator(): Promise<void> {
const appLauncher = await this.selectProject();
const adbHelper = new AdbHelper(appLauncher.getPackager().getProjectPath());
const androidEmulatorManager = new AndroidEmulatorManager(adbHelper);
const emulator = await androidEmulatorManager.startSelection();
if (emulator) {
androidEmulatorManager.tryLaunchEmulatorByName(emulator);
}
}
/**
* Executes the 'react-native run-android' command
*/
@ -111,16 +123,17 @@ export class CommandPaletteHandler {
appLauncher.setReactNativeVersions(versions);
return this.executeCommandInContext("runAndroid", appLauncher.getWorkspaceFolder(), () => {
const platform = <AndroidPlatform>this.createPlatform(appLauncher, "android", AndroidPlatform, target);
return platform.beforeStartPackager()
.then(() => {
return platform.startPackager();
})
.then(() => {
return platform.runApp(/*shouldLaunchInAllDevices*/true);
})
.then(() => {
return platform.disableJSDebuggingMode();
});
return platform.resolveVirtualDevice(target)
.then(() => platform.beforeStartPackager())
.then(() => {
return platform.startPackager();
})
.then(() => {
return platform.runApp(/*shouldLaunchInAllDevices*/true);
})
.then(() => {
return platform.disableJSDebuggingMode();
});
});
});
});
@ -138,7 +151,8 @@ export class CommandPaletteHandler {
TargetPlatformHelper.checkTargetPlatformSupport("ios");
return this.executeCommandInContext("runIos", appLauncher.getWorkspaceFolder(), () => {
const platform = <IOSPlatform>this.createPlatform(appLauncher, "ios", IOSPlatform, target);
return platform.beforeStartPackager()
return platform.resolveVirtualDevice(target)
.then(() => platform.beforeStartPackager())
.then(() => {
return platform.startPackager();
})

Просмотреть файл

@ -8,6 +8,8 @@ import {PackagerStatusIndicator, PackagerStatus} from "./packagerStatusIndicator
import {SettingsHelper} from "./settingsHelper";
import {OutputChannelLogger} from "./log/OutputChannelLogger";
import * as nls from "vscode-nls";
import { isBoolean } from "util";
import { IVirtualDevice } from "./VirtualDeviceManager";
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize = nls.loadMessageBundle();
@ -39,6 +41,10 @@ export class GeneralMobilePlatform {
this.runArguments = this.getRunArguments();
}
public resolveVirtualDevice(target: string): Promise<IVirtualDevice | null> {
return Promise.resolve(null);
}
public runApp(): Promise<void> {
this.logger.info(localize("ConnectedToPackager", "Connected to packager. You can now open your app in the simulator."));
return Promise.resolve();
@ -81,6 +87,40 @@ export class GeneralMobilePlatform {
return Promise.resolve();
}
public static removeRunArgument(runArguments: any[], optName: string, binary: boolean) {
const optIdx = runArguments.indexOf(optName);
if (optIdx > -1) {
if (binary) {
runArguments.splice(optIdx, 1);
}
else {
runArguments.splice(optIdx, 2);
}
}
}
public static setRunArgument(runArguments: any[], optName: string, value: string | boolean) {
const isBinary = isBoolean(value);
const optIdx = runArguments.indexOf(optName);
if (optIdx > -1) {
if (isBinary && !value) {
GeneralMobilePlatform.removeRunArgument(runArguments, optName, true);
}
if (!isBinary) {
runArguments[optIdx + 1] = value;
}
}
else {
if (isBinary && value) {
runArguments.push(optName);
}
if (!isBinary) {
runArguments.push(optName);
runArguments.push(value);
}
}
}
public static getOptFromRunArgs(runArguments: any[], optName: string, binary: boolean = false): any {
if (runArguments.length > 0) {
const optIdx = runArguments.indexOf(optName);

Просмотреть файл

@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as path from "path";
import * as fs from "fs";
import stripJsonComments = require("strip-json-comments");
export interface IConfiguration {
name: string;
platform?: string;
target?: string;
type?: string;
request?: string;
}
export interface ILaunchScenarios {
configurations?: IConfiguration[];
}
export class LaunchScenariosManager {
private pathToLaunchFile: string;
private launchScenarios: ILaunchScenarios;
constructor(rootPath: string) {
this.pathToLaunchFile = path.resolve(rootPath, ".vscode", "launch.json");
}
public getLaunchScenarios(): ILaunchScenarios {
return this.launchScenarios;
}
private getFirstScenarioIndexByParams(scenario: IConfiguration): number | null {
if (this.launchScenarios.configurations) {
for (let i = 0; i < this.launchScenarios.configurations.length; i++) {
const config = this.launchScenarios.configurations[i];
if (scenario.name === config.name &&
scenario.platform === config.platform &&
scenario.type === config.type &&
scenario.request === config.request) {
return i;
}
}
}
return null;
}
private writeLaunchScenarios(launch: ILaunchScenarios = this.launchScenarios): void {
if (fs.existsSync(this.pathToLaunchFile)) {
fs.writeFileSync(this.pathToLaunchFile, JSON.stringify(this.launchScenarios, null, 4));
}
}
public readLaunchScenarios(): void {
if (fs.existsSync(this.pathToLaunchFile)) {
const content = fs.readFileSync(this.pathToLaunchFile, "utf8");
this.launchScenarios = JSON.parse(stripJsonComments(content));
}
}
public updateLaunchScenario(launchArgs: any, updates: any) {
this.readLaunchScenarios();
let launchConfigIndex = this.getFirstScenarioIndexByParams(launchArgs);
const launchScenarios = this.getLaunchScenarios();
if (launchConfigIndex !== null && launchScenarios.configurations) {
Object.assign(launchScenarios.configurations[launchConfigIndex], updates);
this.writeLaunchScenarios(launchScenarios);
}
}
}

Просмотреть файл

@ -221,6 +221,7 @@ function isSupportedVersion(version: string): boolean {
}
function registerReactNativeCommands(context: vscode.ExtensionContext): void {
registerVSCodeCommand(context, "launchAndroidSimulator-preview", ErrorHelper.getInternalError(InternalErrorCode.FailedToStartAndroidEmulator), () => CommandPaletteHandler.launchAndroidEmulator());
registerVSCodeCommand(context, "runAndroidSimulator-preview", ErrorHelper.getInternalError(InternalErrorCode.FailedToRunOnAndroid), () => CommandPaletteHandler.runAndroid("simulator"));
registerVSCodeCommand(context, "runAndroidDevice-preview", ErrorHelper.getInternalError(InternalErrorCode.FailedToRunOnAndroid), () => CommandPaletteHandler.runAndroid("device"));
registerVSCodeCommand(context, "runIosSimulator-preview", ErrorHelper.getInternalError(InternalErrorCode.FailedToRunOnIos), () => CommandPaletteHandler.runIos("simulator"));

Просмотреть файл

@ -8,30 +8,6 @@ import * as path from "path";
suite("generalMobilePlatform", function () {
suite("extensionContext", function () {
suite("getOptFromRunArgs", function() {
test("should return undefined if arguments are empty", function () {
const args: any[] = [];
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", true), undefined);
});
test("should return correct result for binary parameters", function () {
const args: any[] = ["--param1", "param2"];
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", true), true);
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "param2", true), true);
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--unknown", true), undefined);
});
test("should return correct result for non-binary parameters", function () {
const args: any[] = ["--param1", "value1", "--param2=value2", "param3=value3", "param4value4"];
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", false), "value1");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param2", false), "value2");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1"), "value1");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param2"), "value2");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "param3", false), "value3");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "param4", false), undefined);
});
});
suite("getEnvArgument", function() {
const origEnv: any = {"test1": "origEnv", "test2": "origEnv", "test3": "origEnv"};
@ -81,5 +57,90 @@ suite("generalMobilePlatform", function () {
"test5": "envFile"});
});
});
suite("runArguments", function() {
let mockRunArguments: any[] = [];
const paramWithValue = "--paramWithValue";
const binaryParam = "--binaryParam";
suite("getOptFromRunArgs", function() {
test("should return undefined if arguments are empty", function () {
const args: any[] = [];
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", true), undefined);
});
test("should return correct result for binary parameters", function () {
const args: any[] = ["--param1", "param2"];
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", true), true);
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "param2", true), true);
assert.strictEqual(GeneralMobilePlatform.getOptFromRunArgs(args, "--unknown", true), undefined);
});
test("should return correct result for non-binary parameters", function () {
const args: any[] = ["--param1", "value1", "--param2=value2", "param3=value3", "param4value4"];
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1", false), "value1");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param2", false), "value2");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param1"), "value1");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "--param2"), "value2");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "param3", false), "value3");
assert.equal(GeneralMobilePlatform.getOptFromRunArgs(args, "param4", false), undefined);
});
});
suite("removeRunArgument", function() {
setup(() => {
mockRunArguments = [paramWithValue, "value", binaryParam];
});
test("existing binary parameter should be removed from runArguments", function() {
GeneralMobilePlatform.removeRunArgument(mockRunArguments, binaryParam, true);
assert.deepEqual(mockRunArguments, [paramWithValue, "value"]);
});
test("existing parameter and its value should be removed from runArguments", function() {
GeneralMobilePlatform.removeRunArgument(mockRunArguments, paramWithValue, false);
assert.deepEqual(mockRunArguments, [binaryParam]);
});
test("nothing should happen if try to remove not existing parameter", function() {
GeneralMobilePlatform.removeRunArgument(mockRunArguments, "--undefined", false);
assert.deepEqual(mockRunArguments, [paramWithValue, "value", binaryParam]);
GeneralMobilePlatform.removeRunArgument(mockRunArguments, "--undefined", true);
assert.deepEqual(mockRunArguments, [paramWithValue, "value", binaryParam]);
});
});
suite("setRunArgument", function() {
setup(() => {
mockRunArguments = [paramWithValue, "value", binaryParam];
});
test("new binary parameter should be added to runArguments", function() {
GeneralMobilePlatform.setRunArgument(mockRunArguments, "--newBinaryParam", true);
assert.deepEqual(mockRunArguments, [paramWithValue, "value", binaryParam, "--newBinaryParam"]);
});
test("new parameter with value and its value should be added to runArguments", function() {
GeneralMobilePlatform.setRunArgument(mockRunArguments, "--newParamWithValue", "itsValue");
assert.deepEqual(mockRunArguments, [paramWithValue, "value", binaryParam, "--newParamWithValue", "itsValue"]);
});
test("value of existing parameter with value should be overwritten by new value", function() {
GeneralMobilePlatform.setRunArgument(mockRunArguments, paramWithValue, "newValue");
assert.deepEqual(mockRunArguments, [paramWithValue, "newValue", binaryParam]);
});
test("new binary parameter should not be added to runArguments if its value if false", function() {
GeneralMobilePlatform.setRunArgument(mockRunArguments, "--newBinaryParam", false);
assert.deepEqual(mockRunArguments, [paramWithValue, "value", binaryParam]);
});
test("existing binary parameter should be removed from runArguments if its value if false", function() {
GeneralMobilePlatform.setRunArgument(mockRunArguments, binaryParam, false);
assert.deepEqual(mockRunArguments, [paramWithValue, "value"]);
});
});
});
});
});

Просмотреть файл

@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as fs from "fs";
import * as path from "path";
import { LaunchScenariosManager } from "../../src/extension/launchScenariosManager";
import * as assert from "assert";
suite("LaunchScenarioManager", function() {
const tmpPath = path.resolve(__dirname, "..", "resources", "tmp");
const launchPath = path.resolve(tmpPath, ".vscode", "launch.json");
const launchContent = {
version: "0.2.0",
configurations: [
{
name: "Debug Android",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "launch",
platform: "android",
target: "simulator"
},
{
name: "Debug Android (Hermes) - Experimental",
cwd: "${workspaceFolder}",
type: "reactnativedirect-preview",
request: "launch",
platform: "android",
env: {
env1: "value1",
env2: "value2"
}
},
{
name: "Attach to Hermes application - Experimental",
cwd: "${workspaceFolder}",
type: "reactnativedirect-preview",
request: "attach"
},
{
name: "Debug iOS",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "launch",
platform: "ios"
},
{
name: "Attach to packager",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "attach"
},
{
name: "Debug in Exponent",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "launch",
platform: "exponent"
},
{
name: "Debug in Exponent (LAN)",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "launch",
platform: "exponent",
expoHostType: "lan"
},
{
name: "Debug in Exponent (Local)",
cwd: "${workspaceFolder}",
type: "reactnative-preview",
request: "launch",
platform: "exponent",
expoHostType: "local"
}
]
};
suiteSetup(() => {
fs.mkdirSync(tmpPath);
fs.mkdirSync(path.resolve(tmpPath, ".vscode"));
});
suiteTeardown(() => {
fs.unlinkSync(launchPath);
fs.rmdirSync(path.resolve(tmpPath, ".vscode"));
fs.rmdirSync(tmpPath);
});
setup(() => {
fs.writeFileSync(launchPath, JSON.stringify(launchContent, null, 4));
});
suite("updateLaunchScenario", function() {
function autogenerateUpdateAndCheck(configIndex: number, updates: any) {
const config = Object.assign({}, launchContent.configurations[configIndex]);
Object.assign(config, {
otherParam: "value1",
otherObject: {
innerParam: "value2"
}
});
const result = Object.assign({}, launchContent);
Object.assign(result.configurations[configIndex], updates);
tryUpdateAndCheck(config, updates, result);
}
function tryUpdateAndCheck(config: any, updates: any, result: any) {
const manager = new LaunchScenariosManager(tmpPath);
manager.updateLaunchScenario(config, updates);
const launchObject = JSON.parse(fs.readFileSync(launchPath).toString());
console.log(launchObject);
console.log(result);
assert.deepStrictEqual(launchObject, result);
}
test("should overwrite existing parameters for proper configuration", function() {
autogenerateUpdateAndCheck(2, {env:{env1: "newValue"}});
});
test("should add new parameters to proper configuration", function() {
autogenerateUpdateAndCheck(5, {env:{env1: "newValue1", env2: "newValue2"}});
});
test("should nothing happens if launch.json do not contains config", function() {
const config = {
name: "Debug Android",
type: "reactnative-preview",
request: "launch",
platform: "android",
};
let configCopy = Object.assign({}, config);
tryUpdateAndCheck(Object.assign(configCopy, {name: "Other name"}), {param: "value1"}, launchContent);
configCopy = Object.assign({}, config);
tryUpdateAndCheck(Object.assign(configCopy, {type: "Other type"}), {param: "value2"}, launchContent);
configCopy = Object.assign({}, config);
tryUpdateAndCheck(Object.assign(configCopy, {request: "Other request"}), {param: "value3"}, launchContent);
configCopy = Object.assign({}, config);
tryUpdateAndCheck(Object.assign(configCopy, {platform: "Other platform"}), {param: "value4"}, launchContent);
});
});
});

Просмотреть файл

@ -6,7 +6,8 @@
"cwd": "${workspaceFolder}",
"type": "reactnative-preview",
"request": "launch",
"platform": "android"
"platform": "android",
"target": "simulator"
},
{
"name": "Debug Android (Hermes) - Experimental",

Просмотреть файл

@ -5,7 +5,12 @@ import * as cp from "child_process";
import { SmokeTestsConstants } from "./smokeTestsConstants";
import { sleep } from "./utilities";
export interface IDevice {
id: string;
isOnline: boolean;
}
export class AndroidEmulatorHelper {
private static EMULATOR_START_TIMEOUT = 120;
public static androidEmulatorPort = 5554;
public static androidEmulatorName = `emulator-${AndroidEmulatorHelper.androidEmulatorPort}`;
@ -17,6 +22,50 @@ export class AndroidEmulatorHelper {
return process.env.ANDROID_EMULATOR;
}
public static getOnlineDevices(): IDevice[] {
const devices = AndroidEmulatorHelper.getConnectedDevices();
return devices.filter(device => device.isOnline);
}
public static getConnectedDevices(): IDevice[] {
const devices = cp.execSync("adb devices").toString();
return AndroidEmulatorHelper.parseConnectedDevices(devices);
}
private static parseConnectedDevices(input: string): IDevice[] {
let result: IDevice[] = [];
let regex = new RegExp("^(\\S+)\\t(\\S+)$", "mg");
let match = regex.exec(input);
while (match != null) {
result.push({ id: match[1], isOnline: match[2] === "device"});
match = regex.exec(input);
}
return result;
}
public static async waitUntilEmulatorStarting(): Promise<void> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(() => {
cleanup();
reject(`Could not start the emulator within ${AndroidEmulatorHelper.EMULATOR_START_TIMEOUT} seconds.`);
}, AndroidEmulatorHelper.EMULATOR_START_TIMEOUT * 1000);
const bootCheckInterval = setInterval(async () => {
const connectedDevices = AndroidEmulatorHelper.getOnlineDevices();
if (connectedDevices.length > 0) {
console.log(`*** Android emulator has been started.`);
cleanup();
resolve();
}
}, 1000);
const cleanup = () => {
clearTimeout(rejectTimeout);
clearInterval(bootCheckInterval);
};
});
}
public static async runAndroidEmulator() {
this.terminateAndroidEmulator();
// Boot options for emulator - https://developer.android.com/studio/run/emulator-commandline
@ -67,12 +116,13 @@ export class AndroidEmulatorHelper {
// Terminates emulator "emulator-PORT" if it exists, where PORT is 5554 by default
public static terminateAndroidEmulator() {
let devices = cp.execSync("adb devices").toString().trim();
let devices = this.getOnlineDevices();
console.log("*** Checking for running android emulators...");
if (devices !== "List of devices attached") {
// Check if we already have a running emulator, and terminate it if it so
console.log(`Terminating Android '${this.androidEmulatorName}'...`);
cp.execSync(`adb -s ${this.androidEmulatorName} emu kill`, {stdio: "inherit"});
if (devices.length !== 0) {
devices.forEach((device) => {
console.log(`Terminating Android '${device.id}'...`);
cp.execSync(`adb -s ${device.id} emu kill`, {stdio: "inherit"});
});
} else {
console.log("*** No running android emulators found");
}

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as path from "path";
import * as fs from "fs";
import * as cp from "child_process";
import * as request from "request";
@ -176,6 +177,46 @@ export interface ExpoLaunch {
failed: boolean;
}
function getEmulatorsNamesList(): string[] {
const res = cp.execSync("emulator -list-avds");
let emulatorsList: string[] = [];
if (res) {
const resString = res.toString();
emulatorsList = resString.split(/\r?\n|\r/g);
}
return emulatorsList;
}
export function waitUntilLaunchScenarioTargetUpdate(workspaceRoot: string): Promise<boolean> {
return new Promise((resolve) => {
const LAUNCH_UPDATE_TIMEOUT = 30;
const rejectTimeout = setTimeout(() => {
cleanup();
resolve(false);
}, LAUNCH_UPDATE_TIMEOUT * 1000);
const bootCheckInterval = setInterval(async () => {
const isUpdated = isLaunchScenarioTargetUpdate(workspaceRoot);
if (isUpdated) {
cleanup();
resolve(true);
}
}, 1000);
const cleanup = () => {
clearTimeout(rejectTimeout);
clearInterval(bootCheckInterval);
};
});
}
export function isLaunchScenarioTargetUpdate(workspaceRoot: string): boolean {
const pathToLaunchFile = path.resolve(workspaceRoot, ".vscode", "launch.json");
const emulatorsList = getEmulatorsNamesList();
const firstEmulatorName = emulatorsList[0];
return findStringInFile(pathToLaunchFile, `"target": "${firstEmulatorName}"`);
}
export async function waitForRunningPackager(filePath: string) {
let awaitRetries: number = 5;
let retry = 1;