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

Commit f7626d4

Browse files
committed
Merge pull request #31 from telerik/fatme/emulate
Share emulator services
2 parents 199ff9e + 30a3290 commit f7626d4

File tree

6 files changed

+336
-0
lines changed

6 files changed

+336
-0
lines changed

bootstrap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ $injector.require("androidDevice", "./common/mobile/android/android-device");
4444

4545
$injector.require("devicesServices", "./common/mobile/mobile-core/devices-services");
4646
$injector.require("projectNameValidator", "./common/validators/project-name-validator");
47+
48+
$injector.require("androidEmulatorServices", "./common/mobile/android/android-emulator-services");
49+
$injector.require("iOSEmulatorServices", "./common/mobile/ios/ios-emulator-services");
50+
$injector.require("wp8EmulatorServices", "./common/mobile/wp8/wp8-emulator-services");

definitions/iconv-lite.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module "iconv-lite" {
2+
export function extendNodeEncodings(): void;
3+
}

definitions/mobile.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,25 @@ declare module Mobile {
149149
publishTelerikAppManager: boolean;
150150
hostPlatformsForDeploy: string[];
151151
}
152+
153+
interface IAvdInfo {
154+
target: string;
155+
targetNum: number;
156+
path: string;
157+
device?: string;
158+
name?: string;
159+
abi?: string;
160+
skin?: string;
161+
sdcard?: string;
162+
}
163+
164+
interface IEmulatorPlatformServices {
165+
checkAvailability(): IFuture<void>;
166+
startEmulator(app: string, image?: string) : IFuture<void>;
167+
}
168+
169+
interface IEmulatorSettingsService {
170+
canStart(platform: string): IFuture<boolean>;
171+
minVersion: number;
172+
}
152173
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
///<reference path="../../../.d.ts"/>
2+
"use strict";
3+
4+
import Fiber = require("fibers");
5+
import Future = require("fibers/future");
6+
import iconv = require("iconv-lite");
7+
import os = require("os");
8+
import osenv = require("osenv");
9+
import path = require("path");
10+
import util = require("util");
11+
import hostInfo = require("../../../common/host-info");
12+
import MobileHelper = require("./../mobile-helper");
13+
14+
class AndroidEmulatorServices implements Mobile.IEmulatorPlatformServices {
15+
private static ANDROID_DIR_NAME = ".android";
16+
private static AVD_DIR_NAME = "avd";
17+
private static INI_FILES_MASK = /^(.*)\.ini$/i;
18+
private static ENCODING_MASK = /^avd\.ini\.encoding=(.*)$/;
19+
20+
constructor(private $logger: ILogger,
21+
private $emulatorSettingsService: Mobile.IEmulatorSettingsService,
22+
private $errors: IErrors,
23+
private $childProcess: IChildProcess,
24+
private $fs: IFileSystem,
25+
private $staticConfig: IStaticConfig) {
26+
iconv.extendNodeEncodings();
27+
}
28+
29+
public checkAvailability(): IFuture<void> {
30+
return (() => {
31+
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.Android];
32+
if (!this.$emulatorSettingsService.canStart(platform).wait()) {
33+
this.$errors.fail("The current project does not target Android and cannot be run in the Android emulator.");
34+
}
35+
}).future<void>()();
36+
}
37+
38+
public startEmulator(app: string, appId: string, image?: string) : IFuture<void> {
39+
return (() => {
40+
image = image || this.getBestFit().wait();
41+
this.startEmulatorCore(app, appId, image).wait();
42+
}).future<void>()();
43+
}
44+
45+
private startEmulatorCore(app: string, appId: string, image: string) : IFuture<void> {
46+
return (() => {
47+
// start the emulator, if needed
48+
var runningEmulators = this.getRunningEmulators().wait();
49+
if (runningEmulators.length === 0) {
50+
this.$logger.info("Starting Android emulator with image %s", image);
51+
this.$childProcess.spawn('emulator', ['-avd', image],
52+
{ stdio: ["ignore", "ignore", "ignore"], detached: true }).unref();
53+
}
54+
55+
// adb does not always wait for the emulator to fully startup. wait for this
56+
while (runningEmulators.length === 0) {
57+
this.sleep(1000);
58+
runningEmulators = this.getRunningEmulators().wait();
59+
}
60+
61+
// waits for the boot animation property of the emulator to switch to 'stopped'
62+
this.waitForEmulatorBootToComplete().wait();
63+
64+
// unlock screen
65+
var childProcess = this.$childProcess.spawn("adb", ["-e", "shell", "input","keyevent", "82"]);
66+
this.$fs.futureFromEvent(childProcess, "close").wait();
67+
68+
// install the app
69+
this.$logger.info("installing %s through adb", app);
70+
childProcess = this.$childProcess.spawn('adb', ['-e', 'install', '-r', app]);
71+
this.$fs.futureFromEvent(childProcess, "close").wait();
72+
73+
// run the installed app
74+
this.$logger.info("running %s through adb", app);
75+
childProcess = this.$childProcess.spawn('adb', ['-e', 'shell', 'am', 'start', '-S', appId + "/" + this.$staticConfig.START_PACKAGE_ACTIVITY_NAME],
76+
{ stdio: ["ignore", "ignore", "ignore"], detached: true });
77+
this.$fs.futureFromEvent(childProcess, "close").wait();
78+
//this.$childProcess.exec(util.format("adb -e shell am start -S %s/%s", appId, this.$staticConfig.START_PACKAGE_ACTIVITY_NAME)).wait();
79+
}).future<void>()();
80+
}
81+
82+
private sleep(ms: number): void {
83+
var fiber = Fiber.current;
84+
setTimeout(() => fiber.run(), ms);
85+
Fiber.yield();
86+
}
87+
88+
private getRunningEmulators(): IFuture<string[]> {
89+
return (() => {
90+
var emulatorDevices: string[] = [];
91+
var outputRaw = this.$childProcess.execFile('adb', ['devices']).wait().split(os.EOL);
92+
_.each(outputRaw, (device: string) => {
93+
var rx = device.match(/^emulator-(\d+)\s+device$/);
94+
if (rx && rx[1]) {
95+
emulatorDevices.push(rx[1]);
96+
}
97+
});
98+
return emulatorDevices;
99+
}).future<string[]>()();
100+
}
101+
102+
private getBestFit(): IFuture<string> {
103+
return (() => {
104+
var minVersion = this.$emulatorSettingsService.minVersion;
105+
106+
var best =_.chain(this.getAvds().wait())
107+
.map(avd => this.getInfoFromAvd(avd).wait())
108+
.max(avd => avd.targetNum)
109+
.value();
110+
111+
return (best.targetNum >= minVersion) ? best.name : null;
112+
}).future<string>()();
113+
}
114+
115+
private getInfoFromAvd(avdName: string): IFuture<Mobile.IAvdInfo> {
116+
return (() => {
117+
var iniFile = path.join(this.avdDir, avdName + ".ini");
118+
var avdInfo: Mobile.IAvdInfo = this.parseAvdFile(avdName, iniFile).wait();
119+
if (avdInfo.path && this.$fs.exists(avdInfo.path).wait()) {
120+
iniFile = path.join(avdInfo.path, "config.ini");
121+
avdInfo = this.parseAvdFile(avdName, iniFile, avdInfo).wait();
122+
}
123+
return avdInfo;
124+
}).future<Mobile.IAvdInfo>()();
125+
}
126+
127+
private parseAvdFile(avdName: string, avdFileName: string, avdInfo: Mobile.IAvdInfo = null): IFuture<Mobile.IAvdInfo> {
128+
return (() => {
129+
// avd files can have different encoding, defined on the first line.
130+
// find which one it is (if any) and use it to correctly read the file contents
131+
var encoding = this.getAvdEncoding(avdFileName).wait();
132+
var contents = this.$fs.readText(avdFileName, encoding).wait().split("\n");
133+
134+
avdInfo = _.reduce(contents, (result: Mobile.IAvdInfo, line:string) => {
135+
var parsedLine = line.split("=");
136+
var key = parsedLine[0];
137+
switch(key) {
138+
case "target":
139+
result.target = parsedLine[1];
140+
result.targetNum = this.readTargetNum(result.target);
141+
break;
142+
case "path": result.path = parsedLine[1]; break;
143+
case "hw.device.name": result.device = parsedLine[1]; break;
144+
case "abi.type": result.abi = parsedLine[1]; break;
145+
case "skin.name": result.skin = parsedLine[1]; break;
146+
case "sdcard.size": result.sdcard = parsedLine[1]; break;
147+
}
148+
return result;
149+
},
150+
avdInfo || <Mobile.IAvdInfo>Object.create(null));
151+
avdInfo.name = avdName;
152+
return avdInfo;
153+
}).future<Mobile.IAvdInfo>()();
154+
}
155+
156+
// Android L is not written as a number in the .ini files, and we need to convert it
157+
private readTargetNum(target: string): number {
158+
var platform = target.replace('android-', '');
159+
var platformNumber = +platform;
160+
if (isNaN(platformNumber)) {
161+
if (platform === "L") {
162+
platformNumber = 20;
163+
}
164+
}
165+
return platformNumber;
166+
}
167+
168+
private getAvdEncoding(avdName: string): IFuture<any> {
169+
return (() => {
170+
// avd files can have different encoding, defined on the first line.
171+
// find which one it is (if any) and use it to correctly read the file contents
172+
var encoding = "utf8";
173+
var contents = this.$fs.readText(avdName, "ascii").wait();
174+
if (contents.length > 0) {
175+
contents = contents.split("\n", 1)[0];
176+
if (contents.length > 0) {
177+
var matches = contents.match(AndroidEmulatorServices.ENCODING_MASK);
178+
if(matches) {
179+
encoding = matches[1];
180+
}
181+
}
182+
}
183+
return encoding;
184+
}).future<any>()();
185+
}
186+
187+
private get androidHomeDir(): string {
188+
return path.join(osenv.home(), AndroidEmulatorServices.ANDROID_DIR_NAME);
189+
}
190+
191+
private get avdDir(): string {
192+
return path.join(this.androidHomeDir, AndroidEmulatorServices.AVD_DIR_NAME);
193+
}
194+
195+
private getAvds(): IFuture<string[]> {
196+
return (() => {
197+
var result:string[] = [];
198+
if (this.$fs.exists(this.avdDir).wait()) {
199+
var entries = this.$fs.readDirectory(this.avdDir).wait();
200+
result = _.select(entries, (e: string) => e.match(AndroidEmulatorServices.INI_FILES_MASK) !== null)
201+
.map((e) => e.match(AndroidEmulatorServices.INI_FILES_MASK)[1]);
202+
}
203+
return result;
204+
}).future<string[]>()();
205+
}
206+
207+
private waitForEmulatorBootToComplete(): IFuture<void> {
208+
return (() => {
209+
var isEmulatorBootCompleted = this.isEmulatorBootCompleted().wait();
210+
while (!isEmulatorBootCompleted) {
211+
this.sleep(3000);
212+
isEmulatorBootCompleted = this.isEmulatorBootCompleted().wait();
213+
}
214+
}).future<void>()();
215+
}
216+
217+
private isEmulatorBootCompleted(): IFuture<boolean> {
218+
return (() => {
219+
var output = this.$childProcess.execFile("adb", ["-e", "shell", "getprop", "dev.bootcomplete"]).wait();
220+
var matches = output.match("1");
221+
return matches && matches.length > 0;
222+
}).future<boolean>()();
223+
}
224+
}
225+
$injector.register("androidEmulatorServices", AndroidEmulatorServices);
226+
227+

mobile/ios/ios-emulator-services.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
///<reference path="../../../.d.ts"/>
2+
"use strict";
3+
4+
import util = require("util");
5+
import hostInfo = require("../../../common/host-info");
6+
import MobileHelper = require("./../mobile-helper");
7+
8+
class IosEmulatorServices implements Mobile.IEmulatorPlatformServices {
9+
constructor(private $logger: ILogger,
10+
private $emulatorSettingsService: Mobile.IEmulatorSettingsService,
11+
private $errors: IErrors,
12+
private $childProcess: IChildProcess) {}
13+
14+
checkAvailability(): IFuture<void> {
15+
return (() => {
16+
if (!hostInfo.isDarwin()) {
17+
this.$errors.fail("iOS Simulator is available only on Mac OS X.");
18+
}
19+
20+
try {
21+
this.$childProcess.exec(util.format("which ", IosEmulatorServices.SimulatorLauncher)).wait();
22+
} catch(err) {
23+
this.$errors.fail("Unable to find ios-sim. Run `npm install -g ios-sim` to install it.");
24+
}
25+
26+
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.iOS];
27+
if (!this.$emulatorSettingsService.canStart(platform).wait()) {
28+
this.$errors.fail("The current project does not target iOS and cannot be run in the iOS Simulator.");
29+
}
30+
}).future<void>()();
31+
}
32+
33+
startEmulator(image: string) : IFuture<void> {
34+
return (() => {
35+
this.$logger.info("Starting iOS Simulator");
36+
this.$childProcess.spawn(IosEmulatorServices.SimulatorLauncher, ["launch", image],
37+
{ stdio: ["ignore", "ignore", "ignore"], detached: true }).unref();
38+
}).future<void>()();
39+
}
40+
41+
private static SimulatorLauncher = "ios-sim";
42+
}
43+
$injector.register("iOSEmulatorServices", IosEmulatorServices);

mobile/wp8/wp8-emulator-services.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
///<reference path="./../../../.d.ts"/>
2+
"use strict";
3+
4+
import path = require("path");
5+
import hostInfo = require("../../../common/host-info");
6+
import MobileHelper = require("./../mobile-helper");
7+
8+
class Wp8EmulatorServices implements Mobile.IEmulatorPlatformServices {
9+
constructor(private $logger: ILogger,
10+
private $emulatorSettingsService: Mobile.IEmulatorSettingsService,
11+
private $errors: IErrors,
12+
private $childProcess: IChildProcess) {}
13+
14+
checkAvailability(): IFuture<void> {
15+
return (() => {
16+
if (!hostInfo.isWindows()) {
17+
this.$errors.fail("Windows Phone Emulator is available only on Windows 8 or later.");
18+
}
19+
20+
var platform = MobileHelper.DevicePlatforms[MobileHelper.DevicePlatforms.WP8];
21+
if (!this.$emulatorSettingsService.canStart(platform).wait()) {
22+
this.$errors.fail("The current project does not target Windows Phone 8 and cannot be run in the Windows Phone emulator.");
23+
}
24+
}).future<void>()();
25+
}
26+
27+
startEmulator(image: string) : IFuture<void> {
28+
return (() => {
29+
this.$logger.info("Starting Windows Phone Emulator");
30+
var emulatorStarter = path.join (process.env.ProgramFiles, Wp8EmulatorServices.WP8_LAUNCHER_PATH, Wp8EmulatorServices.WP8_LAUNCHER);
31+
this.$childProcess.spawn(emulatorStarter, ["/installlaunch", image, "/targetdevice:xd"], { stdio: ["ignore", "ignore", "ignore"], detached: true }).unref();
32+
}).future<void>()();
33+
}
34+
35+
private static WP8_LAUNCHER = "XapDeployCmd.exe";
36+
private static WP8_LAUNCHER_PATH = "Microsoft SDKs\\Windows Phone\\v8.0\\Tools\\XAP Deployment";
37+
}
38+
$injector.register("wp8EmulatorServices", Wp8EmulatorServices);

0 commit comments

Comments
 (0)