Skip to content
This repository was archived by the owner on Feb 2, 2021. It is now read-only.

Expose emulator api #1101

Merged
merged 5 commits into from
Jul 19, 2018
Merged

Expose emulator api #1101

merged 5 commits into from
Jul 19, 2018

Conversation

Fatme
Copy link
Contributor

@Fatme Fatme commented Jul 6, 2018

This PR deletes all legacy, unneeded and duplicated code that is used for working with iOS and Android emulators.

This PR adds the functionalities below:

  • tns devices android --available-devices - shows all available Android genymotion and Android avd emulators (previously only avd emulators were shown)
  • tns devices --available-devices - works correctly and shows all available iOS simulators, Android genymotion and Android avd emulators. (previously does not work)
  • tns run android --device <imageIdentifier> - it is already possible to run on device with specified imageIdentifier (previously it can run only on emulators specified by identifier or name)

This PR exposes emulator api from devicesService. The api consists from the following two methods:

  1. getAvailableEmulators(opts?: {platform?: string}) -> Returns all available and running iOS and Android emulators. The method accepts platform parameter, so the emulators can be filtered by platform. The output from the method is in the following format:
{
    ios: {
        devices: Mobile.IDeviceInfo[],
        errors: string[]
    },
    android: {
        devices: Mobile.IDeviceInfo[],
        errors: string[]
   }
}

Each available android emulator (NOTE: not running emulator) has the properties below:

{
   identifier: null,
   displayName: the name of the emulator,
   model: the same as the displayName,
   version: The android version of emulator,
   vendor: can be Avd or Genymotion,
   status: not running,
   errorHelp: additional information for errors that prevents working with this device,
   isTablet: false,
   type: Emulator,
   imageIdentifier: for genymotion emulators is the id of virtual machine and for avd is the name of .ini file
}

Each running android emulator has the properties below:

{
   identifier: the emulator identifier,
   displayName: the name of the emulator,
   model: the value of 'ro.product.model' property,
   vendor: the value of 'ro.product.brand' property,
   status: running,
   errorHelp: additional information for errors that prevents working with this device,
   isTablet: when device's 'ro.build.characteristics' property contains "tablet" word or when the 'ro.build.version.release' is 3.x,
   type: Device
   imageIdentifier: for genymotion emulators is the id of virtual machine and for avd is the name of .ini file
}
  1. startEmulator(options) - Starts the specified emulator. The emulator can be specified by imageIdentifier, by name or by emulator object. For android it is possible to pass --timeout option. This option describes the timeout in miliseconds that {N} CLI will wait emulator boot to complete. It might take too long time on some machines to start native android emulator, so this option is useful in such cases.

child-process.ts Outdated
try {
return this.spawnFromEvent(command, args, "close", options, spawnFromEventOptions);
} catch (err) {
this.$logger.trace(`Error from trySpawnAwaitCloseEvent method. More info: ${err}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trySpawnAwaitCloseEvent -> trySpawnFromCloseEvent

private $options: ICommonOptions) { }

public allowedParameters = [this.$stringParameter];

public async execute(args: string[]): Promise<void> {
if (this.$options.availableDevices) {
await this.$emulatorImageService.listAvailableEmulators(this.$mobileHelper.validatePlatformName(args[0]));
const platform = this.$mobileHelper.normalizePlatformName(args[0]);
if (!platform && args[0]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we stop using this.$mobileHelper.validatePlatformName?

Copy link
Contributor Author

@Fatme Fatme Jul 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because when tns devices --available-devices command was executed, an error was thrown from validatePlatformName method. I fixed this behaviour.
Actually when the platform is not valid (for example when tns devices invalidplatform), normalizePlatformName method will return undefined and the check above

if (!platform && args[0]) {

will throw an error.

* @param input The options that can be passed to filter the result.
* @returns {Promise<Mobile.IListEmulatorsOutput>} Dictionary with the following format: { ios: { devices: Mobile.IDeviceInfo[], errors: string[] }, android: { devices: Mobile.IDeviceInfo[], errors: string[]}}.
*/
getAvailableEmulators(input?: Mobile.IListEmulatorsOptions): Promise<Mobile.IListEmulatorsOutput>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input -> options?

* @param availableDevices - All available devices.
* @returns {Promise<Mobile.IDeviceInfo>} The running emulator if such can be found by provided emulatorId or null otherwise
*/
getRunningEmulator(emulatorId: string, availableEmulators?: Mobile.IDeviceInfo[]): Promise<Mobile.IDeviceInfo>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the parameters and their XML comments do not match (emulatorId vs emulatorIdOrName and availableEmulators vs availableDevices)

return output
.split(EOL)
.filter(device => !!device)
.filter(device => device !== "List of devices attached"); // TODO: Consider to move this to constants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to constants or remove the TODO

}

this.$logger.printInfoMessageOnSameLine(".");
await this.sleep(10000);
await sleep(10000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exactly 10 secs? Can't we check the boot state more often? (e.g. with an incremental sleep)

case "hw.device.name": result.device = parsedLine[1]; break;
case "abi.type": result.abi = parsedLine[1]; break;
case "skin.name": result.skin = parsedLine[1]; break;
case "sdcard.size": result.sdcard = parsedLine[1]; break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also apply the getAvdManagerDeviceInfo approach here (result[key] = parsedLine[1])

@@ -40,6 +48,16 @@ export class ListDevicesCommand implements ICommand {
this.$logger.out(table.toString());
}
}

private outputEmulators(title: string, emulators: Mobile.IDeviceInfo[]) {
this.$logger.out(title);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can support --json here and just print the stringified information (just an idea, not a merge stopper)

@@ -40,6 +48,16 @@ export class ListDevicesCommand implements ICommand {
this.$logger.out(table.toString());
}
}

private outputEmulators(title: string, emulators: Mobile.IDeviceInfo[]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename this to printEmulatorsInfo

constants.ts Outdated
@@ -39,6 +45,11 @@ export class DeviceDiscoveryEventNames {
static DEVICE_LOST = "deviceLost";
}

export class EmulatorDiscoveryNames {
static AVAILABLE_EMULATOR_FOUND = "availableEmulatorFound";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe emulatorImageFound and emulatorImageLost are better names

@@ -587,6 +587,23 @@ export function getValueFromNestedObject(obj: any, key: string): any {
return _.head(_getValueRecursive(obj, key));
}

export function getWinRegPropertyValue(key: string, propertyName: string): Promise<string> {
return new Promise((resolve, reject) => {
const Winreg = require("winreg");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interestingly, we had a service winreg, added in this PR. However, it's been deleted at some point, but this line still makes me think we have the service.
Anyway, we can get back the winreg service in a separate PR.

const output = await this.$childProcess.execFile<string>(this.adbFilePath, ['devices']);
return output
.split(EOL)
.filter(device => !!device)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can combine the two filters:

return output
    .split(EOL)
    .filter(line => !!line && line !== "List of devices attached");

const availableEmulatorsOutput = await this.getAvailableEmulatorsCore();
const genies = availableEmulatorsOutput.devices;
const runningEmulatorIds = await this.getRunningEmulatorIds(adbDevicesOutput);
const runningEmulators = await Promise.all(runningEmulatorIds.map(emulatorId => this.getRunningEmulatorData(emulatorId, genies)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case one of the action fails, we'll not get data for any of the Genymotion emulators. Consider using settlePromises

try {
return this.$childProcess.spawn(pathToPlayer, ["--vm-name", imageIdentifier], { stdio: "ignore", detached: true }).unref();
} catch (err) {
this.$logger.trace(`error while starting emulator. More info: ${err}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this catch will never catch anything as we are using async version of the spawn. In case there's error, it will be thrown on nextTick. As we do not have .on("error" handler for the currently spawned process, the error will be raised as UncaughtException and could lead to many unexpected behaviors.
Also why is the process fired and forget?

return (<string>_.first(output.split(EOL))).trim();
}

private get defaultPlayerPath() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type

return "/Applications/Genymotion.app/Contents/MacOS/player.app/Contents/MacOS/player";
}

if (this.$hostInfo.isWindows) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just return "";

*/
const result: any = await getWinRegPropertyValue("\\Software\\Oracle\\VirtualBox", "InstallDir");
searchPath = result && result.value ? result.value : null;
} catch (err) { /* */ }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least trace this error.

@rosen-vladimirov
Copy link
Collaborator

I've tried this code and I have issues with Genymotion emulators - I do not have paths to Genymotion executables in my PATH, so my expectation was that the emulators will not be available. However, tns devices --available-devices listed my Genymotion emulators.

$ ./bin/tns devices --available-devices

Available emulators
┌─────────────┬──────────┬────────────┬──────────────────────────────────────┐
│ Device Name │ Platform │ Version    │ Device Identifier                    │
│ Nexus 6     │ Android  │ android-24 │ GN6                                  │
│ Nexus 5X    │ Android  │ android-26 │ Nexus_5X_API_26_x86                  │
│ GN5X_API24  │ Android  │ 7.0        │ daa35603-f7ca-46a4-90f5-1116e8cd187d │
└─────────────┴──────────┴────────────┴──────────────────────────────────────┘

Connected devices & emulators
Searching for devices...
iTunes is not installed. Install it on your system and run this command again.
Cannot find connected devices. Reconnect any connected devices, verify that your system recognizes them, and run this command again.

I've tried running my app on one of them by passing the name:

$ ../../bin/tns run android --device GN5X_API24
Searching for devices...
Cannot find connected devices.
Emulator start failed with: No emulator image available for emulator 'GN5X_API24'.
To list currently connected devices and verify that the specified identifier exists, run 'tns device'.
To list available emulator images, run 'tns device <Platform> --available-devices'.

After that I've tried with the identifier:

$ ../../bin/tns run android --device  daa35603-f7ca-46a4-90f5-1116e8cd187d
Searching for devices...
Error: spawn player ENOENT
    at _errnoException (util.js:992:11)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:190:19)
    at onErrorNT (internal/child_process.js:372:16)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickCallback (internal/process/next_tick.js:180:9)

@rosen-vladimirov
Copy link
Collaborator

Also I'm unable to run my normal emulator via its name (not sure if this is supported):

$ ../../bin/tns run android --device "Nexus 5X"
Searching for devices...
Cannot find connected devices.
Emulator start failed with: No emulator image available for emulator 'Nexus 5X'.
To list currently connected devices and verify that the specified identifier exists, run 'tns device'.
To list available emulator images, run 'tns device <Platform> --available-devices'.

@rosen-vladimirov
Copy link
Collaborator

When passing the identifier, emulator starts, but CLI hangs and after some time, it fails with error:

Emulator start failed with: Cannot run your app in the native emulator. Increase the timeout of the operation with the --timeout option or try to restart your adb server with 'adb kill-server' command. Alternatively, run the Android Virtual Device manager and increase the allocated RAM for the virtual device.

After that, I execute the same command (note - the emulator is already running) and the operation fails with different error:

$ ../../bin/tns run android --device "Nexus_5X_API_26_x86"
Searching for devices...
Waiting for emulator device initialization...
Could not find device by specified identifier 'Nexus_5X_API_26_x86'. To list currently connected devices and verify that the specified identifier exists, run 'tns device'.

In fact, tns device shows the following:

rosen@DESKTOP-A5EQ3J3 MINGW64 /d/Rosen/Work/nativescript-cli/scratch/app13 (fatme/emulator-api)
$ ../../bin/tns device

Connected devices & emulators
Searching for devices...
iTunes is not installed. Install it on your system and run this command again.
┌───┬────────────────┬──────────┬───────────────────┬──────────┬───────────┐
│ # │ Device Name    │ Platform │ Device Identifier │ Type     │ Status    │
│ 1 │ sdk_gphone_x86 │ Android  │ emulator-5554     │ Emulator │ Connected │
└───┴────────────────┴──────────┴───────────────────┴──────────┴───────────┘

The strange thing is that the name I see for my image is: Nexus 5X API 26 x86, not Nexus_5X_API_26_x86: https://www.screencast.com/t/KjXdsy3jYu
Also it looks like it tried to start additional emulator from the same image.

@Fatme Fatme force-pushed the fatme/emulator-api branch 8 times, most recently from 7735819 to eee133c Compare July 19, 2018 14:37
Fatme added 5 commits July 19, 2018 18:33
…onged exists but actually it is a valid device that exists.

Fix failing QA tests:
	`tns run android --device fakeId --path TestApp --justlaunch` throws an error
@Fatme Fatme force-pushed the fatme/emulator-api branch from eee133c to 867bc37 Compare July 19, 2018 15:41
@Fatme Fatme merged commit 8d326ab into master Jul 19, 2018
@Fatme Fatme deleted the fatme/emulator-api branch July 19, 2018 16:24
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants