This repository was archived by the owner on Feb 2, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
Share emulator services #31
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
declare module "iconv-lite" { | ||
export function extendNodeEncodings(): void; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
///<reference path="../../../.d.ts"/> | ||
"use strict"; | ||
|
||
import Fiber = require("fibers"); | ||
import Future = require("fibers/future"); | ||
import iconv = require("iconv-lite"); | ||
import os = require("os"); | ||
import osenv = require("osenv"); | ||
import path = require("path"); | ||
import util = require("util"); | ||
import hostInfo = require("../../../common/host-info"); | ||
import MobileHelper = require("./../mobile-helper"); | ||
|
||
class AndroidEmulatorServices implements Mobile.IEmulatorPlatformServices { | ||
private static ANDROID_DIR_NAME = ".android"; | ||
private static AVD_DIR_NAME = "avd"; | ||
private static INI_FILES_MASK = /^(.*)\.ini$/i; | ||
private static ENCODING_MASK = /^avd\.ini\.encoding=(.*)$/; | ||
|
||
constructor(private $logger: ILogger, | ||
private $emulatorSettingsService: Mobile.IEmulatorSettingsService, | ||
private $errors: IErrors, | ||
private $childProcess: IChildProcess, | ||
private $fs: IFileSystem, | ||
private $staticConfig: IStaticConfig) { | ||
iconv.extendNodeEncodings(); | ||
} | ||
|
||
public checkAvailability(): IFuture<void> { | ||
return (() => { | ||
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.Android]; | ||
if (!this.$emulatorSettingsService.canStart(platform).wait()) { | ||
this.$errors.fail("The current project does not target Android and cannot be run in the Android emulator."); | ||
} | ||
}).future<void>()(); | ||
} | ||
|
||
public startEmulator(app: string, appId: string, image?: string) : IFuture<void> { | ||
return (() => { | ||
image = image || this.getBestFit().wait(); | ||
this.startEmulatorCore(app, appId, image).wait(); | ||
}).future<void>()(); | ||
} | ||
|
||
private startEmulatorCore(app: string, appId: string, image: string) : IFuture<void> { | ||
return (() => { | ||
// start the emulator, if needed | ||
var runningEmulators = this.getRunningEmulators().wait(); | ||
if (runningEmulators.length === 0) { | ||
this.$logger.info("Starting Android emulator with image %s", image); | ||
this.$childProcess.spawn('emulator', ['-avd', image], | ||
{ stdio: ["ignore", "ignore", "ignore"], detached: true }).unref(); | ||
} | ||
|
||
// adb does not always wait for the emulator to fully startup. wait for this | ||
while (runningEmulators.length === 0) { | ||
this.sleep(1000); | ||
runningEmulators = this.getRunningEmulators().wait(); | ||
} | ||
|
||
// waits for the boot animation property of the emulator to switch to 'stopped' | ||
this.waitForEmulatorBootToComplete().wait(); | ||
|
||
// unlock screen | ||
var childProcess = this.$childProcess.spawn("adb", ["-e", "shell", "input","keyevent", "82"]); | ||
this.$fs.futureFromEvent(childProcess, "close").wait(); | ||
|
||
// install the app | ||
this.$logger.info("installing %s through adb", app); | ||
childProcess = this.$childProcess.spawn('adb', ['-e', 'install', '-r', app]); | ||
this.$fs.futureFromEvent(childProcess, "close").wait(); | ||
|
||
// run the installed app | ||
this.$logger.info("running %s through adb", app); | ||
childProcess = this.$childProcess.spawn('adb', ['-e', 'shell', 'am', 'start', '-S', appId + "/" + this.$staticConfig.START_PACKAGE_ACTIVITY_NAME], | ||
{ stdio: ["ignore", "ignore", "ignore"], detached: true }); | ||
this.$fs.futureFromEvent(childProcess, "close").wait(); | ||
//this.$childProcess.exec(util.format("adb -e shell am start -S %s/%s", appId, this.$staticConfig.START_PACKAGE_ACTIVITY_NAME)).wait(); | ||
}).future<void>()(); | ||
} | ||
|
||
private sleep(ms: number): void { | ||
var fiber = Fiber.current; | ||
setTimeout(() => fiber.run(), ms); | ||
Fiber.yield(); | ||
} | ||
|
||
private getRunningEmulators(): IFuture<string[]> { | ||
return (() => { | ||
var emulatorDevices: string[] = []; | ||
var outputRaw = this.$childProcess.execFile('adb', ['devices']).wait().split(os.EOL); | ||
_.each(outputRaw, (device: string) => { | ||
var rx = device.match(/^emulator-(\d+)\s+device$/); | ||
if (rx && rx[1]) { | ||
emulatorDevices.push(rx[1]); | ||
} | ||
}); | ||
return emulatorDevices; | ||
}).future<string[]>()(); | ||
} | ||
|
||
private getBestFit(): IFuture<string> { | ||
return (() => { | ||
var minVersion = this.$emulatorSettingsService.minVersion; | ||
|
||
var best =_.chain(this.getAvds().wait()) | ||
.map(avd => this.getInfoFromAvd(avd).wait()) | ||
.max(avd => avd.targetNum) | ||
.value(); | ||
|
||
return (best.targetNum >= minVersion) ? best.name : null; | ||
}).future<string>()(); | ||
} | ||
|
||
private getInfoFromAvd(avdName: string): IFuture<Mobile.IAvdInfo> { | ||
return (() => { | ||
var iniFile = path.join(this.avdDir, avdName + ".ini"); | ||
var avdInfo: Mobile.IAvdInfo = this.parseAvdFile(avdName, iniFile).wait(); | ||
if (avdInfo.path && this.$fs.exists(avdInfo.path).wait()) { | ||
iniFile = path.join(avdInfo.path, "config.ini"); | ||
avdInfo = this.parseAvdFile(avdName, iniFile, avdInfo).wait(); | ||
} | ||
return avdInfo; | ||
}).future<Mobile.IAvdInfo>()(); | ||
} | ||
|
||
private parseAvdFile(avdName: string, avdFileName: string, avdInfo: Mobile.IAvdInfo = null): IFuture<Mobile.IAvdInfo> { | ||
return (() => { | ||
// avd files can have different encoding, defined on the first line. | ||
// find which one it is (if any) and use it to correctly read the file contents | ||
var encoding = this.getAvdEncoding(avdFileName).wait(); | ||
var contents = this.$fs.readText(avdFileName, encoding).wait().split("\n"); | ||
|
||
avdInfo = _.reduce(contents, (result: Mobile.IAvdInfo, line:string) => { | ||
var parsedLine = line.split("="); | ||
var key = parsedLine[0]; | ||
switch(key) { | ||
case "target": | ||
result.target = parsedLine[1]; | ||
result.targetNum = this.readTargetNum(result.target); | ||
break; | ||
case "path": result.path = parsedLine[1]; break; | ||
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; | ||
} | ||
return result; | ||
}, | ||
avdInfo || <Mobile.IAvdInfo>Object.create(null)); | ||
avdInfo.name = avdName; | ||
return avdInfo; | ||
}).future<Mobile.IAvdInfo>()(); | ||
} | ||
|
||
// Android L is not written as a number in the .ini files, and we need to convert it | ||
private readTargetNum(target: string): number { | ||
var platform = target.replace('android-', ''); | ||
var platformNumber = +platform; | ||
if (isNaN(platformNumber)) { | ||
if (platform === "L") { | ||
platformNumber = 20; | ||
} | ||
} | ||
return platformNumber; | ||
} | ||
|
||
private getAvdEncoding(avdName: string): IFuture<any> { | ||
return (() => { | ||
// avd files can have different encoding, defined on the first line. | ||
// find which one it is (if any) and use it to correctly read the file contents | ||
var encoding = "utf8"; | ||
var contents = this.$fs.readText(avdName, "ascii").wait(); | ||
if (contents.length > 0) { | ||
contents = contents.split("\n", 1)[0]; | ||
if (contents.length > 0) { | ||
var matches = contents.match(AndroidEmulatorServices.ENCODING_MASK); | ||
if(matches) { | ||
encoding = matches[1]; | ||
} | ||
} | ||
} | ||
return encoding; | ||
}).future<any>()(); | ||
} | ||
|
||
private get androidHomeDir(): string { | ||
return path.join(osenv.home(), AndroidEmulatorServices.ANDROID_DIR_NAME); | ||
} | ||
|
||
private get avdDir(): string { | ||
return path.join(this.androidHomeDir, AndroidEmulatorServices.AVD_DIR_NAME); | ||
} | ||
|
||
private getAvds(): IFuture<string[]> { | ||
return (() => { | ||
var result:string[] = []; | ||
if (this.$fs.exists(this.avdDir).wait()) { | ||
var entries = this.$fs.readDirectory(this.avdDir).wait(); | ||
result = _.select(entries, (e: string) => e.match(AndroidEmulatorServices.INI_FILES_MASK) !== null) | ||
.map((e) => e.match(AndroidEmulatorServices.INI_FILES_MASK)[1]); | ||
} | ||
return result; | ||
}).future<string[]>()(); | ||
} | ||
|
||
private waitForEmulatorBootToComplete(): IFuture<void> { | ||
return (() => { | ||
var isEmulatorBootCompleted = this.isEmulatorBootCompleted().wait(); | ||
while (!isEmulatorBootCompleted) { | ||
this.sleep(3000); | ||
isEmulatorBootCompleted = this.isEmulatorBootCompleted().wait(); | ||
} | ||
}).future<void>()(); | ||
} | ||
|
||
private isEmulatorBootCompleted(): IFuture<boolean> { | ||
return (() => { | ||
var output = this.$childProcess.execFile("adb", ["-e", "shell", "getprop", "dev.bootcomplete"]).wait(); | ||
var matches = output.match("1"); | ||
return matches && matches.length > 0; | ||
}).future<boolean>()(); | ||
} | ||
} | ||
$injector.register("androidEmulatorServices", AndroidEmulatorServices); | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
///<reference path="../../../.d.ts"/> | ||
"use strict"; | ||
|
||
import util = require("util"); | ||
import hostInfo = require("../../../common/host-info"); | ||
import MobileHelper = require("./../mobile-helper"); | ||
|
||
class IosEmulatorServices implements Mobile.IEmulatorPlatformServices { | ||
constructor(private $logger: ILogger, | ||
private $emulatorSettingsService: Mobile.IEmulatorSettingsService, | ||
private $errors: IErrors, | ||
private $childProcess: IChildProcess) {} | ||
|
||
checkAvailability(): IFuture<void> { | ||
return (() => { | ||
if (!hostInfo.isDarwin()) { | ||
this.$errors.fail("iOS Simulator is available only on Mac OS X."); | ||
} | ||
|
||
try { | ||
this.$childProcess.exec(util.format("which ", IosEmulatorServices.SimulatorLauncher)).wait(); | ||
} catch(err) { | ||
this.$errors.fail("Unable to find ios-sim. Run `npm install -g ios-sim` to install it."); | ||
} | ||
|
||
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.iOS]; | ||
if (!this.$emulatorSettingsService.canStart(platform).wait()) { | ||
this.$errors.fail("The current project does not target iOS and cannot be run in the iOS Simulator."); | ||
} | ||
}).future<void>()(); | ||
} | ||
|
||
startEmulator(image: string) : IFuture<void> { | ||
return (() => { | ||
this.$logger.info("Starting iOS Simulator"); | ||
this.$childProcess.spawn(IosEmulatorServices.SimulatorLauncher, ["launch", image], | ||
{ stdio: ["ignore", "ignore", "ignore"], detached: true }).unref(); | ||
}).future<void>()(); | ||
} | ||
|
||
private static SimulatorLauncher = "ios-sim"; | ||
} | ||
$injector.register("iOSEmulatorServices", IosEmulatorServices); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a merge stopper, but consider adding a new line here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
///<reference path="./../../../.d.ts"/> | ||
"use strict"; | ||
|
||
import path = require("path"); | ||
import hostInfo = require("../../../common/host-info"); | ||
import MobileHelper = require("./../mobile-helper"); | ||
|
||
class Wp8EmulatorServices implements Mobile.IEmulatorPlatformServices { | ||
constructor(private $logger: ILogger, | ||
private $emulatorSettingsService: Mobile.IEmulatorSettingsService, | ||
private $errors: IErrors, | ||
private $childProcess: IChildProcess) {} | ||
|
||
checkAvailability(): IFuture<void> { | ||
return (() => { | ||
if (!hostInfo.isWindows()) { | ||
this.$errors.fail("Windows Phone Emulator is available only on Windows 8 or later."); | ||
} | ||
|
||
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.WP8]; | ||
if (!this.$emulatorSettingsService.canStart(platform).wait()) { | ||
this.$errors.fail("The current project does not target Windows Phone 8 and cannot be run in the Windows Phone emulator."); | ||
} | ||
}).future<void>()(); | ||
} | ||
|
||
startEmulator(image: string) : IFuture<void> { | ||
return (() => { | ||
this.$logger.info("Starting Windows Phone Emulator"); | ||
var emulatorStarter = path.join (process.env.ProgramFiles, Wp8EmulatorServices.WP8_LAUNCHER_PATH, Wp8EmulatorServices.WP8_LAUNCHER); | ||
this.$childProcess.spawn(emulatorStarter, ["/installlaunch", image, "/targetdevice:xd"], { stdio: ["ignore", "ignore", "ignore"], detached: true }).unref(); | ||
}).future<void>()(); | ||
} | ||
|
||
private static WP8_LAUNCHER = "XapDeployCmd.exe"; | ||
private static WP8_LAUNCHER_PATH = "Microsoft SDKs\\Windows Phone\\v8.0\\Tools\\XAP Deployment"; | ||
} | ||
$injector.register("wp8EmulatorServices", Wp8EmulatorServices); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a merge stopper, but consider adding a new line here. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we talked in person, it is a good idea to check whether ios-sim is installed. Here is a good place for such check. Consider adding it in this PR, but I am fine to postpone it to another one.