diff --git a/lib/child-process.ts b/lib/child-process.ts
new file mode 100644
index 0000000..c74e0ea
--- /dev/null
+++ b/lib/child-process.ts
@@ -0,0 +1,54 @@
+///
+"use strict";
+
+import child_process = require("child_process");
+import errors = require("./errors");
+import Future = require("fibers/future");
+import util = require("util");
+
+export function exec(command: string): IFuture {
+ var future = new Future();
+
+ child_process.exec(command, (error: Error, stdout: NodeBuffer, stderr: NodeBuffer) => {
+ //console.log(util.format("Executing: %s", command));
+
+ if(error) {
+ errors.fail(util.format("Error %s while executing %s.", error.message, command));
+ } else {
+ future.return(stdout ? stdout.toString() : "");
+ }
+ });
+
+ return future;
+}
+
+export function spawn(command: string, args: string[]): IFuture {
+ let future = new Future();
+ let capturedOut = "";
+ let capturedErr = "";
+
+ let childProcess = child_process.spawn(command, args);
+
+ if(childProcess.stdout) {
+ childProcess.stdout.on("data", (data: string) => {
+ capturedOut += data;
+ });
+ }
+
+ if(childProcess.stderr) {
+ childProcess.stderr.on("data", (data: string) => {
+ capturedErr += data;
+ });
+ }
+
+ childProcess.on("close", (arg: any) => {
+ var exitCode = typeof arg == 'number' ? arg : arg && arg.code;
+ if(exitCode === 0) {
+ future.return(capturedOut ? capturedOut.trim() : null);
+ } else {
+ future.throw(util.format("Command %s with arguments %s failed with exit code %s. Error output: \n %s", command, args.join(" "), exitCode, capturedErr));
+ }
+ });
+
+ return future;
+}
\ No newline at end of file
diff --git a/lib/commands/launch.ts b/lib/commands/launch.ts
index b3b536f..ae87db5 100644
--- a/lib/commands/launch.ts
+++ b/lib/commands/launch.ts
@@ -7,6 +7,6 @@ import iphoneSimulatorLibPath = require("./../iphone-simulator");
export class Command implements ICommand {
public execute(args: string[]): IFuture {
var iphoneSimulator = new iphoneSimulatorLibPath.iPhoneSimulator();
- return iphoneSimulator.run(args[0]);
+ return iphoneSimulator.run(args[0], args[1]);
}
}
\ No newline at end of file
diff --git a/lib/declarations.ts b/lib/declarations.ts
index da53403..66a2043 100644
--- a/lib/declarations.ts
+++ b/lib/declarations.ts
@@ -2,7 +2,7 @@
"use strict";
interface IiPhoneSimulator {
- run(appName: string): IFuture;
+ run(applicationPath: string, applicationIdentifier: string): IFuture;
printDeviceTypes(): IFuture;
printSDKS(): IFuture;
sendNotification(notification: string): IFuture;
@@ -17,22 +17,38 @@ interface ICommandExecutor {
}
interface IDevice {
- device: any; // NodObjC wrapper to device
- deviceIdentifier: string;
- fullDeviceIdentifier: string;
+ name: string;
+ id: string;
+ fullId: string;
runtimeVersion: string;
+ state?: string;
+ rawDevice?: any; // NodObjC wrapper to device
+}
+
+interface ISimctl {
+ launch(deviceId: string, applicationIdentifier: string): IFuture;
+ install(deviceId: string, applicationPath: string): IFuture;
+ uninstall(deviceId: string, applicationIdentifier: string): IFuture;
+ notifyPost(deviceId: string, notification: string): IFuture;
+ getDevices(): IFuture;
}
interface IDictionary {
[key: string]: T;
}
-interface ISimulator {
- validDeviceIdentifiers: string[];
- deviceIdentifiersInfo: string[];
+interface IInteropSimulator {
+ getDevices(): IFuture;
setSimulatedDevice(config: any): void;
}
+interface ISimulator {
+ getDevices(): IFuture;
+ getSdks(): IFuture;
+ run(applicationPath: string, applicationIdentifier: string): IFuture;
+ sendNotification(notification: string): IFuture;
+}
+
interface IExecuteOptions {
canRunMainLoop: boolean;
appPath?: string;
@@ -42,5 +58,10 @@ interface ISdk {
displayName: string;
version: string;
rootPath: string;
- sdkInfo(): string;
+}
+
+interface IXcodeVersionData {
+ major: string;
+ minor: string;
+ build: string;
}
\ No newline at end of file
diff --git a/lib/iphone-interop-simulator-base.ts b/lib/iphone-interop-simulator-base.ts
new file mode 100644
index 0000000..2f882fb
--- /dev/null
+++ b/lib/iphone-interop-simulator-base.ts
@@ -0,0 +1,275 @@
+///
+"use strict";
+
+import child_process = require("child_process");
+import errors = require("./errors");
+import fs = require("fs");
+import Future = require("fibers/future");
+import options = require("./options");
+import os = require("os");
+import path = require("path");
+import util = require("util");
+import utils = require("./utils");
+
+var $ = require("NodObjC");
+
+export class IPhoneInteropSimulatorBase {
+ constructor(private simulator: IInteropSimulator) { }
+
+ private static FOUNDATION_FRAMEWORK_NAME = "Foundation";
+ private static APPKIT_FRAMEWORK_NAME = "AppKit";
+
+ private static DVT_FOUNDATION_RELATIVE_PATH = "../SharedFrameworks/DVTFoundation.framework";
+ private static DEV_TOOLS_FOUNDATION_RELATIVE_PATH = "../OtherFrameworks/DevToolsFoundation.framework";
+ private static CORE_SIMULATOR_RELATIVE_PATH = "Library/PrivateFrameworks/CoreSimulator.framework";
+ private static SIMULATOR_FRAMEWORK_RELATIVE_PATH_LEGACY = "Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
+ private static SIMULATOR_FRAMEWORK_RELATIVE_PATH = "../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
+
+ private static DEFAULT_TIMEOUT_IN_SECONDS = 90;
+
+ public run(appPath: string): IFuture {
+ return this.execute(this.launch, { canRunMainLoop: true, appPath: appPath });
+ }
+
+ private launch(appPath: string): void {
+ var sessionDelegate = $.NSObject.extend("DTiPhoneSimulatorSessionDelegate");
+ sessionDelegate.addMethod("session:didEndWithError:", "v@:@@", function(self: any, sel: any, sess: any, error: any) {
+ IPhoneInteropSimulatorBase.logSessionInfo(error, "Session ended without errors.", "Session ended with error ");
+ process.exit(0);
+ });
+ sessionDelegate.addMethod("session:didStart:withError:", "v@:@c@", function(self: any, sel: string, session: any, started: boolean, error:any) {
+ IPhoneInteropSimulatorBase.logSessionInfo(error, "Session started without errors.", "Session started with error ");
+
+ console.log(`${appPath}: ${session("simulatedApplicationPID")}`);
+ if(options.exit) {
+ process.exit(0);
+ }
+ });
+ sessionDelegate.register();
+
+ var appSpec = this.getClassByName("DTiPhoneSimulatorApplicationSpecifier")("specifierWithApplicationPath", $(appPath));
+ var config = this.getClassByName("DTiPhoneSimulatorSessionConfig")("alloc")("init")("autorelease");
+ config("setApplicationToSimulateOnStart", appSpec);
+ config("setSimulatedApplicationShouldWaitForDebugger", options.waitForDebugger);
+
+ var sdkRoot = options.sdkVersion ? $(this.getSdkRootPathByVersion(options.sdkVersion)) : this.getClassByName("DTiPhoneSimulatorSystemRoot")("defaultRoot");
+ config("setSimulatedSystemRoot", sdkRoot);
+
+ var simulator = this.simulator;
+ if(options.device) {
+ let devices = simulator.getDevices().wait();
+ var validDeviceIdentifiers = _.map(devices, device => device.id);
+ if(!_.contains(validDeviceIdentifiers, options.device)) {
+ errors.fail("Invalid device identifier %s. Valid device identifiers are %s.", options.device, utils.stringify(validDeviceIdentifiers));
+ }
+ }
+ simulator.setSimulatedDevice(config);
+
+ if(options.logging) {
+ var logPath = this.createLogPipe(appPath).wait();
+ fs.createReadStream(logPath, { encoding: "utf8" }).pipe(process.stdout);
+ config("setSimulatedApplicationStdErrPath", $(logPath));
+ config("setSimulatedApplicationStdOutPath", $(logPath));
+ } else {
+ if(options.stderr) {
+ config("setSimulatedApplicationStdErrPath", $(options.stderr));
+ }
+ if(options.stdout) {
+ config("setSimulatedApplicationStdOutPath", $(options.stdout));
+ }
+ }
+
+ if (options.args) {
+ var args = options.args.trim().split(/\s+/);
+ var nsArgs = $.NSMutableArray("array");
+ args.forEach((x: string) => nsArgs("addObject", $(x)));
+ config("setSimulatedApplicationLaunchArgs", nsArgs);
+ }
+
+ config("setLocalizedClientName", $("ios-sim-portable"));
+
+ var sessionError: any = new Buffer("");
+ var timeoutParam = IPhoneInteropSimulatorBase.DEFAULT_TIMEOUT_IN_SECONDS;
+ if (options.timeout || options.timeout === 0) {
+ var parsedValue = parseInt(options.timeout);
+ if(!isNaN(parsedValue) && parsedValue > 0) {
+ timeoutParam = parsedValue;
+ }
+ else {
+ console.log(util.format("Specify the timeout in number of seconds to wait. It should be greater than 0. Default value %s seconds will be used.", IPhoneInteropSimulatorBase.DEFAULT_TIMEOUT_IN_SECONDS.toString()));
+ }
+ }
+
+ var time = $.NSNumber("numberWithDouble", timeoutParam);
+ var timeout = time("doubleValue");
+
+ var session = this.getClassByName("DTiPhoneSimulatorSession")("alloc")("init")("autorelease");
+ var delegate = sessionDelegate("alloc")("init");
+ session("setDelegate", delegate);
+
+ if(!session("requestStartWithConfig", config, "timeout", timeout, "error", sessionError)) {
+ errors.fail("Could not start simulator session ", sessionError);
+ }
+ }
+
+ protected execute(action: (appPath?: string) => any, opts: IExecuteOptions): IFuture {
+ $.importFramework(IPhoneInteropSimulatorBase.FOUNDATION_FRAMEWORK_NAME);
+ $.importFramework(IPhoneInteropSimulatorBase.APPKIT_FRAMEWORK_NAME);
+
+ var pool = $.NSAutoreleasePool("alloc")("init");
+
+ var developerDirectoryPath = this.findDeveloperDirectory().wait();
+ if(!developerDirectoryPath) {
+ errors.fail("Unable to find developer directory");
+ }
+
+ this.loadFrameworks(developerDirectoryPath);
+
+ let result = action.apply(this, [opts.appPath]);
+
+ var future = new Future();
+ if(opts.canRunMainLoop) {
+ // Keeps the Node loop running
+ (function runLoop() {
+ if($.CFRunLoopRunInMode($.kCFRunLoopDefaultMode, 0.1, false)) {
+ setTimeout(runLoop, 0);
+ } else {
+ pool("release");
+ future.return(result);
+ }
+ }());
+ } else {
+ future.return(result);
+ }
+ return future;
+ }
+
+ private loadFrameworks(developerDirectoryPath: string): void {
+ this.loadFramework(path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.DVT_FOUNDATION_RELATIVE_PATH));
+ this.loadFramework(path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.DEV_TOOLS_FOUNDATION_RELATIVE_PATH));
+
+ if(fs.existsSync(path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.CORE_SIMULATOR_RELATIVE_PATH))) {
+ this.loadFramework(path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.CORE_SIMULATOR_RELATIVE_PATH));
+ }
+
+ var platformsError: string = null;
+ var dvtPlatformClass = this.getClassByName("DVTPlatform");
+ if(!dvtPlatformClass("loadAllPlatformsReturningError", platformsError)) {
+ errors.fail("Unable to loadAllPlatformsReturningError ", platformsError);
+ }
+
+ var simulatorFrameworkPath = path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.SIMULATOR_FRAMEWORK_RELATIVE_PATH_LEGACY);
+ if(!fs.existsSync(simulatorFrameworkPath)) {
+ simulatorFrameworkPath = path.join(developerDirectoryPath, IPhoneInteropSimulatorBase.SIMULATOR_FRAMEWORK_RELATIVE_PATH);
+ }
+ this.loadFramework(simulatorFrameworkPath);
+ }
+
+ private loadFramework(frameworkPath: string) {
+ var bundle = $.NSBundle("bundleWithPath", $(frameworkPath));
+ if(!bundle("load")) {
+ errors.fail("Unable to load ", frameworkPath);
+ }
+ }
+
+ private findDeveloperDirectory(): IFuture {
+ var future = new Future();
+ var capturedOut = "";
+ var capturedErr = "";
+
+ var childProcess = child_process.spawn("xcode-select", ["-print-path"]);
+
+ if(childProcess.stdout) {
+ childProcess.stdout.on("data", (data: string) => {
+ capturedOut += data;
+ });
+ }
+
+ if(childProcess.stderr) {
+ childProcess.stderr.on("data", (data: string) => {
+ capturedErr += data;
+ });
+ }
+
+ childProcess.on("close", (arg: any) => {
+ var exitCode = typeof arg == 'number' ? arg : arg && arg.code;
+ if(exitCode === 0) {
+ future.return(capturedOut ? capturedOut.trim() : null);
+ } else {
+ future.throw(util.format("Command xcode-select -print-path failed with exit code %s. Error output: \n %s", exitCode, capturedErr));
+ }
+ });
+
+ return future;
+ }
+
+ private getClassByName(className: string): any {
+ return $.classDefinition.getClassByName(className);
+ }
+
+ private static logSessionInfo(error: any, successfulMessage: string, errorMessage: string): void {
+ if(error) {
+ console.log(util.format("%s %s", errorMessage, error));
+ process.exit(1);
+ }
+
+ console.log(successfulMessage);
+ }
+
+ private getSdkRootPathByVersion(version: string): string {
+ var sdks = this.getInstalledSdks();
+ var sdk = _.find(sdks, sdk => sdk.version === version);
+ if(!sdk) {
+ errors.fail("Unable to find installed sdk with version %s. Verify that you have specified correct version and the sdk with that version is installed.", version);
+ }
+
+ return sdk.rootPath;
+ }
+
+ private getInstalledSdks(): ISdk[] {
+ var systemRootClass = this.getClassByName("DTiPhoneSimulatorSystemRoot");
+ var roots = systemRootClass("knownRoots");
+ var count = roots("count");
+
+ var sdks: ISdk[] = [];
+ for(var index=0; index < count; index++) {
+ var root = roots("objectAtIndex", index);
+
+ var displayName = root("sdkDisplayName").toString();
+ var version = root("sdkVersion").toString();
+ var rootPath = root("sdkRootPath").toString();
+
+ sdks.push(new Sdk(displayName, version, rootPath));
+ }
+
+ return sdks;
+ }
+
+ private createLogPipe(appPath: string): IFuture {
+ var future = new Future();
+ var logPath = path.join(path.dirname(appPath), "." + path.basename(appPath, ".app") + ".log");
+
+ var command = util.format("rm -f \"%s\" && mkfifo \"%s\"", logPath, logPath);
+ child_process.exec(command, (error: Error, stdout: NodeBuffer, stderr: NodeBuffer) => {
+ if(error) {
+ future.throw(error);
+ } else {
+ future.return(logPath);
+ }
+ });
+
+ return future;
+ }
+}
+
+class Sdk implements ISdk {
+ constructor(public displayName: string,
+ public version: string,
+ public rootPath: string) { }
+
+ public sdkInfo(): string {
+ return [util.format(" Display Name: %s", this.displayName),
+ util.format(" Version: %s", this.version),
+ util.format(" Root path: %s", this.rootPath)].join(os.EOL);
+ }
+}
\ No newline at end of file
diff --git a/lib/iphone-simulator-xcode-5.ts b/lib/iphone-simulator-xcode-5.ts
index a7461a3..e7799ea 100644
--- a/lib/iphone-simulator-xcode-5.ts
+++ b/lib/iphone-simulator-xcode-5.ts
@@ -2,13 +2,20 @@
"use strict";
import errors = require("./errors");
+import Future = require("fibers/future");
import options = require("./options");
import utils = require("./utils");
import util = require("util");
var $ = require("NodObjC");
-export class XCode5Simulator implements ISimulator {
+import iPhoneSimulatorBaseLib = require("./iphone-interop-simulator-base");
+
+export class XCode5Simulator extends iPhoneSimulatorBaseLib.IPhoneInteropSimulatorBase implements IInteropSimulator {
+
+ constructor() {
+ super(this);
+ }
private static DEFAULT_DEVICE_IDENTIFIER = "iPhone";
@@ -22,20 +29,38 @@ export class XCode5Simulator implements ISimulator {
"iPad-Retina-64-bit": "iPad Retina (64-bit)"
};
- public get validDeviceIdentifiers(): string[] {
- return _.keys(XCode5Simulator.allowedDeviceIdentifiers);
+ public getDevices(): IFuture {
+ return (() => {
+ let devices: IDevice[] = [];
+ _.each(_.keys(XCode5Simulator.allowedDeviceIdentifiers), deviceName => {
+ devices.push({
+ name: deviceName,
+ id: deviceName,
+ fullId: deviceName,
+ runtimeVersion: ""
+ });
+ });
+
+ return devices;
+ }).future()();
}
- public get deviceIdentifiersInfo(): string[] {
- return _.keys(XCode5Simulator.allowedDeviceIdentifiers);
+ public getSdks(): IFuture {
+ return (() => {
+ return ([]);
+ }).future()();
}
public setSimulatedDevice(config:any): void {
config("setSimulatedDeviceInfoName", $(this.deviceIdentifier));
}
+ public sendNotification(notification: string): IFuture {
+ return Future.fromResult();
+ }
+
private get deviceIdentifier(): string {
- var identifier = options.device || XCode5Simulator.DEFAULT_DEVICE_IDENTIFIER;
+ let identifier = options.device || XCode5Simulator.DEFAULT_DEVICE_IDENTIFIER;
return XCode5Simulator.allowedDeviceIdentifiers[identifier];
}
}
\ No newline at end of file
diff --git a/lib/iphone-simulator-xcode-6.ts b/lib/iphone-simulator-xcode-6.ts
index 124501e..f7c5b36 100644
--- a/lib/iphone-simulator-xcode-6.ts
+++ b/lib/iphone-simulator-xcode-6.ts
@@ -7,50 +7,40 @@ import util = require("util");
import os = require("os");
var $ = require("NodObjC");
-export class XCode6Simulator implements ISimulator {
+import iPhoneSimulatorBaseLib = require("./iphone-interop-simulator-base");
+
+export class XCode6Simulator extends iPhoneSimulatorBaseLib.IPhoneInteropSimulatorBase implements IInteropSimulator {
private static DEVICE_IDENTIFIER_PREFIX = "com.apple.CoreSimulator.SimDeviceType";
private static DEFAULT_DEVICE_IDENTIFIER = "iPhone-4s";
-
- private availableDevices: IDictionary;
+ private cachedDevices: IDevice[] = null;
constructor() {
- this.availableDevices = Object.create(null);
- }
-
- public get validDeviceIdentifiers(): string[] {
- var devices = this.getDevicesInfo();
- return _.map(devices, device => device.deviceIdentifier);
- }
-
- public get deviceIdentifiersInfo(): string[] {
- var devices = this.getDevicesInfo();
- return _.map(devices, device => util.format("Device Identifier: %s. %sRuntime Version: %s %s", device.fullDeviceIdentifier, os.EOL, device.runtimeVersion, os.EOL));
+ super(this);
}
public setSimulatedDevice(config: any): void {
- var device = this.getDeviceByIdentifier(this.deviceIdentifier);
+ let device = this.getDeviceByName().device;
config("setDevice", device);
}
public getSimulatedDevice(): any {
- return this.getDeviceByIdentifier(this.deviceIdentifier);
+ return this.getDeviceByName().device;
}
- private getDevicesInfo(): IDevice[] {
- return _(this.getAvailableDevices())
- .map(_.identity)
- .flatten()
- .value();
+ public getDevices(): IFuture {
+ return this.execute(() => this.devices, { canRunMainLoop: false });
}
- private get deviceIdentifier(): string {
- return options.device || XCode6Simulator.DEFAULT_DEVICE_IDENTIFIER;
+ public getSdks(): IFuture {
+ return this.execute(() => this.sdks, { canRunMainLoop: false });
}
- private getAvailableDevices(): IDictionary {
- if(_.isEmpty(this.availableDevices)) {
+ private get devices(): IDevice[] {
+ if(!this.cachedDevices) {
+ this.cachedDevices = [];
+
var deviceSet = $.classDefinition.getClassByName("SimDeviceSet")("defaultSet");
var devices = deviceSet("availableDevices");
var count = devices("count");
@@ -64,34 +54,70 @@ export class XCode6Simulator implements ISimulator {
var runtimeVersion = device("runtime")("versionString").toString();
- if(!this.availableDevices[deviceIdentifier]) {
- this.availableDevices[deviceIdentifier] = [];
- }
-
- this.availableDevices[deviceIdentifier].push({
- device: device,
- deviceIdentifier: deviceIdentifierWithoutPrefix,
- fullDeviceIdentifier: this.buildFullDeviceIdentifier(deviceIdentifier),
- runtimeVersion: runtimeVersion
+ this.cachedDevices.push({
+ name: "",
+ id: deviceIdentifierWithoutPrefix,
+ fullId: this.buildFullDeviceIdentifier(deviceIdentifier),
+ runtimeVersion: runtimeVersion,
+ rawDevice: device
});
}
}
}
- return this.availableDevices;
+ return this.cachedDevices;
+ }
+
+ private get sdks(): ISdk[] {
+ var systemRootClass = $.classDefinition.getClassByName("DTiPhoneSimulatorSystemRoot");
+ var roots = systemRootClass("knownRoots");
+ var count = roots("count");
+
+ var sdks: ISdk[] = [];
+ for(var index=0; index < count; index++) {
+ var root = roots("objectAtIndex", index);
+
+ var displayName = root("sdkDisplayName").toString();
+ var version = root("sdkVersion").toString();
+ var rootPath = root("sdkRootPath").toString();
+
+ sdks.push({
+ displayName: displayName,
+ version: version,
+ rootPath: rootPath
+ });
+ }
+
+ return sdks;
}
- private getDeviceByIdentifier(deviceIdentifier: string): any {
- var availableDevices = this.getAvailableDevices();
- if(!_.isEmpty(availableDevices)) {
- var fullDeviceIdentifier = this.buildFullDeviceIdentifier(deviceIdentifier);
- var selectedDevice = availableDevices[fullDeviceIdentifier];
- if(selectedDevice) {
- return selectedDevice[0].device;
+ public sendNotification(notification: string): IFuture {
+ let action = () => {
+ let device = this.getSimulatedDevice();
+ if (!device) {
+ errors.fail("Could not find device.");
+ }
+
+ let result = device("postDarwinNotification", $(notification), "error", null);
+ if (!result) {
+ errors.fail("Could not send notification: " + notification);
}
+ };
+
+ return this.execute(action, { canRunMainLoop: false });
+ }
+
+ private get deviceName(): string {
+ return options.device || XCode6Simulator.DEFAULT_DEVICE_IDENTIFIER;
+ }
+
+ private getDeviceByName(): any {
+ let device = _.find(this.devices, (device) => device.name === this.deviceName);
+ if(!device) {
+ errors.fail("Unable to find device with name ", this.deviceName);
}
- errors.fail("Unable to find device with identifier ", deviceIdentifier);
+ return device;
}
private buildFullDeviceIdentifier(deviceIdentifier: string): string {
diff --git a/lib/iphone-simulator-xcode-7.ts b/lib/iphone-simulator-xcode-7.ts
new file mode 100644
index 0000000..0e7e462
--- /dev/null
+++ b/lib/iphone-simulator-xcode-7.ts
@@ -0,0 +1,112 @@
+///
+"use strict";
+
+import childProcess = require("./child-process");
+import errors = require("./errors");
+
+import options = require("./options");
+import path = require("path");
+import { Simctl } from "./simctl";
+import util = require("util");
+import utils = require("./utils");
+import xcode = require("./xcode");
+var $ = require("NodObjC");
+
+export class XCode7Simulator implements ISimulator {
+ private static DEVICE_IDENTIFIER_PREFIX = "com.apple.CoreSimulator.SimDeviceType";
+ private static DEFAULT_DEVICE_NAME = "iPhone-4s";
+
+ private simctl: ISimctl = null;
+
+ constructor() {
+ this.simctl = new Simctl();
+ }
+
+ public getDevices(): IFuture {
+ return this.simctl.getDevices();
+ }
+
+ public getSdks(): IFuture {
+ return (() => {
+ let devices = this.simctl.getDevices().wait();
+ return _.map(devices, device => device.runtimeVersion);
+ }).future()();
+ }
+
+ public run(applicationPath: string, applicationIdentifier: string): IFuture {
+ return (() => {
+ let device = this.getDeviceToRun().wait();
+
+ if(!this.isDeviceBooted(device)) {
+ this.startSimulator(device).wait();
+ // startSimulaltor doesn't always finish immediately, and the subsequent
+ // install fails since the simulator is not running.
+ // Give it some time to start before we attempt installing.
+ utils.sleep(1000);
+ }
+
+ this.simctl.install(device.id, applicationPath).wait();
+ this.simctl.launch(device.id, applicationIdentifier).wait();
+ }).future()();
+ }
+
+ public sendNotification(notification: string): IFuture {
+ return (() => {
+ let device = this.getBootedDevice().wait();
+ if (!device) {
+ errors.fail("Could not find device.");
+ }
+
+ this.simctl.notifyPost("booted", notification).wait();
+ }).future()();
+ }
+
+ private getDeviceToRun(): IFuture {
+ return (() => {
+ let devices = this.simctl.getDevices().wait();
+ let result = _.find(devices, (device: IDevice) => {
+ if(options.sdkVersion && !options.device) {
+ return device.runtimeVersion === options.sdkVersion;
+ }
+
+ if(options.device && !options.sdkVersion) {
+ return device.name === options.device;
+ }
+
+ if(options.device && options.sdkVersion) {
+ return device.runtimeVersion === options.sdkVersion && device.name === options.device;
+ }
+
+ if(!options.sdkVersion && !options.device) {
+ return this.isDeviceBooted(device);
+ }
+ });
+
+ if(!result) {
+ let sortedDevices = _.sortBy(devices, (device) => device.runtimeVersion);
+ result = _.last(sortedDevices);
+ }
+
+ return result;
+ }).future()();
+ }
+
+ private isDeviceBooted(device: IDevice): boolean {
+ return device.state === 'Booted';
+ }
+
+ private getBootedDevice(): IFuture {
+ return (() => {
+ let devices = this.simctl.getDevices().wait();
+ return _.find(devices, device => this.isDeviceBooted(device));
+ }).future()();
+ }
+
+ private startSimulator(device: IDevice): IFuture {
+ return (() => {
+ let simulatorPath = path.resolve(xcode.getPathFromXcodeSelect().wait(), "Applications", "Simulator.app");
+ let args = [simulatorPath, '--args', '-CurrentDeviceUDID', device.id];
+ childProcess.spawn("open", args).wait();
+ }).future()();
+ }
+}
\ No newline at end of file
diff --git a/lib/iphone-simulator.ts b/lib/iphone-simulator.ts
index 7690c68..f762efd 100644
--- a/lib/iphone-simulator.ts
+++ b/lib/iphone-simulator.ts
@@ -10,328 +10,77 @@ import util = require("util");
import errors = require("./errors");
import options = require("./options");
-import utils = require("./utils");
+import xcode = require("./xcode");
+
+import xcode7SimulatorLib = require("./iphone-simulator-xcode-7");
import xcode6SimulatorLib = require("./iphone-simulator-xcode-6");
import xcode5SimulatorLib = require("./iphone-simulator-xcode-5");
var $ = require("NodObjC");
export class iPhoneSimulator implements IiPhoneSimulator {
+ private simulator: ISimulator = null;
- private static FOUNDATION_FRAMEWORK_NAME = "Foundation";
- private static APPKIT_FRAMEWORK_NAME = "AppKit";
-
- private static DVT_FOUNDATION_RELATIVE_PATH = "../SharedFrameworks/DVTFoundation.framework";
- private static DEV_TOOLS_FOUNDATION_RELATIVE_PATH = "../OtherFrameworks/DevToolsFoundation.framework";
- private static CORE_SIMULATOR_RELATIVE_PATH = "Library/PrivateFrameworks/CoreSimulator.framework";
- private static SIMULATOR_FRAMEWORK_RELATIVE_PATH_LEGACY = "Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
- private static SIMULATOR_FRAMEWORK_RELATIVE_PATH = "../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework";
+ constructor() {
+ this.simulator = this.createSimulator().wait();
+ }
- private static DEFAULT_TIMEOUT_IN_SECONDS = 90;
+ public get validDeviceIdentifiers(): string[] {
+ var devices = this.simulator.getDevices().wait();
+ return _.map(devices, device => device.id);
+ }
- public run(appPath: string): IFuture {
- if(!fs.existsSync(appPath)) {
- errors.fail("Path does not exist ", appPath);
+ public run(applicationPath: string, applicationIdentifier: string): IFuture {
+ if(!fs.existsSync(applicationPath)) {
+ errors.fail("Path does not exist ", applicationPath);
}
- return this.execute(this.launch, { canRunMainLoop: true, appPath: appPath });
+ return this.simulator.run(applicationPath, applicationIdentifier);
}
public printDeviceTypes(): IFuture {
- var action = () => {
- var simulator = this.createSimulator();
- _.each(simulator.deviceIdentifiersInfo, (identifier: any) => console.log(identifier));
- };
-
- return this.execute(action, { canRunMainLoop: false });
+ return (() => {
+ let devices = this.simulator.getDevices().wait();
+ _.each(devices, device => console.log(`Device Identifier: ${device.fullId}. ${os.EOL}Runtime version: ${device.runtimeVersion} ${os.EOL}`));
+ }).future()();
}
public printSDKS(): IFuture {
- var action = () => {
- var sdks = this.getInstalledSdks();
- sdks = _.sortBy(sdks, (sdk: ISdk) => sdk.version);
-
- _.each(sdks, (sdk: ISdk) => console.log(sdk.sdkInfo() + os.EOL));
- };
-
- return this.execute(action, { canRunMainLoop: false });
+ return (() => {
+ let sdks = this.simulator.getSdks().wait();
+ _.each(sdks, (sdk) => console.log([util.format(" Display Name: %s", sdk.displayName),
+ util.format(" Version: %s", sdk.version),
+ util.format(" Root path: %s", sdk.rootPath)].join(os.EOL)) );
+ }).future()();
}
public sendNotification(notification: string): IFuture {
if(!notification) {
- errors.fail("Notification required");
- }
-
- var action = () => {
- var simulator = new xcode6SimulatorLib.XCode6Simulator();
- var device = simulator.getSimulatedDevice();
-
- if (!device) {
- errors.fail("Could not find device");
- }
-
- var result = device("postDarwinNotification", $(notification), "error", null);
- if (!result) {
- errors.fail("Could not send notification: " + notification);
- }
- };
-
- return this.execute(action, { canRunMainLoop: false });
- }
-
- private execute(action: (appPath?: string) => any, opts: IExecuteOptions): IFuture {
- $.importFramework(iPhoneSimulator.FOUNDATION_FRAMEWORK_NAME);
- $.importFramework(iPhoneSimulator.APPKIT_FRAMEWORK_NAME);
-
- var pool = $.NSAutoreleasePool("alloc")("init");
-
- var developerDirectoryPath = this.findDeveloperDirectory().wait();
- if(!developerDirectoryPath) {
- errors.fail("Unable to find developer directory");
- }
-
- this.loadFrameworks(developerDirectoryPath);
-
- action.apply(this, [opts.appPath]);
-
- var future = new Future();
- if(opts.canRunMainLoop) {
- // Keeps the Node loop running
- (function runLoop() {
- if($.CFRunLoopRunInMode($.kCFRunLoopDefaultMode, 0.1, false)) {
- setTimeout(runLoop, 0);
- } else {
- pool("release");
- future.return();
- }
- }());
- } else {
- future.return();
- }
- return future;
- }
-
- private launch(appPath: string): void {
- var sessionDelegate = $.NSObject.extend("DTiPhoneSimulatorSessionDelegate");
- sessionDelegate.addMethod("session:didEndWithError:", "v@:@@", function(self: any, sel: any, sess: any, error: any) {
- iPhoneSimulator.logSessionInfo(error, "Session ended without errors.", "Session ended with error ");
- process.exit(0);
- });
- sessionDelegate.addMethod("session:didStart:withError:", "v@:@c@", function(self: any, sel: string, session: any, started: boolean, error:any) {
- iPhoneSimulator.logSessionInfo(error, "Session started without errors.", "Session started with error ");
-
- console.log(`${appPath}: ${session("simulatedApplicationPID")}`);
- if(options.exit) {
- process.exit(0);
- }
- });
- sessionDelegate.register();
-
- var appSpec = this.getClassByName("DTiPhoneSimulatorApplicationSpecifier")("specifierWithApplicationPath", $(appPath));
- var config = this.getClassByName("DTiPhoneSimulatorSessionConfig")("alloc")("init")("autorelease");
- config("setApplicationToSimulateOnStart", appSpec);
- config("setSimulatedApplicationShouldWaitForDebugger", options.waitForDebugger);
-
- var sdkRoot = options.sdkVersion ? $(this.getSdkRootPathByVersion(options.sdkVersion)) : this.getClassByName("DTiPhoneSimulatorSystemRoot")("defaultRoot");
- config("setSimulatedSystemRoot", sdkRoot);
-
- var simulator = this.createSimulator(config);
- if(options.device) {
- var validDeviceIdentifiers = simulator.validDeviceIdentifiers;
- if(!_.contains(validDeviceIdentifiers, options.device)) {
- errors.fail("Invalid device identifier %s. Valid device identifiers are %s.", options.device, utils.stringify(validDeviceIdentifiers));
- }
- }
- simulator.setSimulatedDevice(config);
-
- if(options.logging) {
- var logPath = this.createLogPipe(appPath).wait();
- fs.createReadStream(logPath, { encoding: "utf8" }).pipe(process.stdout);
- config("setSimulatedApplicationStdErrPath", $(logPath));
- config("setSimulatedApplicationStdOutPath", $(logPath));
- } else {
- if(options.stderr) {
- config("setSimulatedApplicationStdErrPath", $(options.stderr));
- }
- if(options.stdout) {
- config("setSimulatedApplicationStdOutPath", $(options.stdout));
- }
- }
-
- if (options.args) {
- var args = options.args.trim().split(/\s+/);
- var nsArgs = $.NSMutableArray("array");
- args.forEach((x: string) => nsArgs("addObject", $(x)));
- config("setSimulatedApplicationLaunchArgs", nsArgs);
- }
-
- config("setLocalizedClientName", $("ios-sim-portable"));
-
- var sessionError: any = new Buffer("");
- var timeoutParam = iPhoneSimulator.DEFAULT_TIMEOUT_IN_SECONDS;
- if (options.timeout || options.timeout === 0) {
- var parsedValue = parseInt(options.timeout);
- if(!isNaN(parsedValue) && parsedValue > 0) {
- timeoutParam = parsedValue;
- }
- else {
- console.log(util.format("Specify the timeout in number of seconds to wait. It should be greater than 0. Default value %s seconds will be used.", iPhoneSimulator.DEFAULT_TIMEOUT_IN_SECONDS.toString()));
- }
- }
-
- var time = $.NSNumber("numberWithDouble", timeoutParam);
- var timeout = time("doubleValue");
-
- var session = this.getClassByName("DTiPhoneSimulatorSession")("alloc")("init")("autorelease");
- var delegate = sessionDelegate("alloc")("init");
- session("setDelegate", delegate);
-
- if(!session("requestStartWithConfig", config, "timeout", timeout, "error", sessionError)) {
- errors.fail("Could not start simulator session ", sessionError);
- }
- }
-
- private loadFrameworks(developerDirectoryPath: string): void {
- this.loadFramework(path.join(developerDirectoryPath, iPhoneSimulator.DVT_FOUNDATION_RELATIVE_PATH));
- this.loadFramework(path.join(developerDirectoryPath, iPhoneSimulator.DEV_TOOLS_FOUNDATION_RELATIVE_PATH));
-
- if(fs.existsSync(path.join(developerDirectoryPath, iPhoneSimulator.CORE_SIMULATOR_RELATIVE_PATH))) {
- this.loadFramework(path.join(developerDirectoryPath, iPhoneSimulator.CORE_SIMULATOR_RELATIVE_PATH));
- }
-
- var platformsError: string = null;
- var dvtPlatformClass = this.getClassByName("DVTPlatform");
- if(!dvtPlatformClass("loadAllPlatformsReturningError", platformsError)) {
- errors.fail("Unable to loadAllPlatformsReturningError ", platformsError);
+ errors.fail("Notification required.");
}
- var simulatorFrameworkPath = path.join(developerDirectoryPath, iPhoneSimulator.SIMULATOR_FRAMEWORK_RELATIVE_PATH_LEGACY);
- if(!fs.existsSync(simulatorFrameworkPath)) {
- simulatorFrameworkPath = path.join(developerDirectoryPath, iPhoneSimulator.SIMULATOR_FRAMEWORK_RELATIVE_PATH);
- }
- this.loadFramework(simulatorFrameworkPath);
- }
-
- private loadFramework(frameworkPath: string) {
- var bundle = $.NSBundle("bundleWithPath", $(frameworkPath));
- if(!bundle("load")) {
- errors.fail("Unable to load ", frameworkPath);
- }
+ return this.simulator.sendNotification(notification);
}
- private findDeveloperDirectory(): IFuture {
- var future = new Future();
- var capturedOut = "";
- var capturedErr = "";
-
- var childProcess = child_process.spawn("xcode-select", ["-print-path"]);
+ private createSimulator(): IFuture {
+ return (() => {
+ let xcodeVersionData = xcode.getXcodeVersionData().wait();
+ let majorVersion = xcodeVersionData.major;
- if(childProcess.stdout) {
- childProcess.stdout.on("data", (data: string) => {
- capturedOut += data;
- });
- }
+ let simulator: ISimulator = null;
- if(childProcess.stderr) {
- childProcess.stderr.on("data", (data: string) => {
- capturedErr += data;
- });
- }
-
- childProcess.on("close", (arg: any) => {
- var exitCode = typeof arg == 'number' ? arg : arg && arg.code;
- if(exitCode === 0) {
- future.return(capturedOut ? capturedOut.trim() : null);
+ if(majorVersion === "7") {
+ simulator = new xcode7SimulatorLib.XCode7Simulator();
+ } else if (majorVersion === "6") {
+ simulator = new xcode6SimulatorLib.XCode6Simulator();
+ } else if(majorVersion === "5") {
+ simulator = new xcode5SimulatorLib.XCode5Simulator();
} else {
- future.throw(util.format("Command xcode-select -print-path failed with exit code %s. Error output: \n %s", exitCode, capturedErr));
+ errors.fail(`Unsupported xcode version ${xcodeVersionData.major}.`);
}
- });
-
- return future;
- }
- private getClassByName(className: string): any {
- return $.classDefinition.getClassByName(className);
- }
-
- private static logSessionInfo(error: any, successfulMessage: string, errorMessage: string): void {
- if(error) {
- console.log(util.format("%s %s", errorMessage, error));
- process.exit(1);
- }
-
- console.log(successfulMessage);
- }
-
- private createSimulator(config?: any): ISimulator {
- if(!config) {
- config = this.getClassByName("DTiPhoneSimulatorSessionConfig")("alloc")("init")("autorelease");
- }
-
- var simulator: ISimulator;
- if(_.contains(config.methods(), "setDevice:")) {
- simulator = new xcode6SimulatorLib.XCode6Simulator();
- } else {
- simulator = new xcode5SimulatorLib.XCode5Simulator();
- }
-
- return simulator;
- }
-
- private createLogPipe(appPath: string): IFuture {
- var future = new Future();
- var logPath = path.join(path.dirname(appPath), "." + path.basename(appPath, ".app") + ".log");
-
- var command = util.format("rm -f \"%s\" && mkfifo \"%s\"", logPath, logPath);
- child_process.exec(command, (error: Error, stdout: NodeBuffer, stderr: NodeBuffer) => {
- if(error) {
- future.throw(error);
- } else {
- future.return(logPath);
- }
- });
-
- return future;
- }
-
- private getInstalledSdks(): ISdk[] {
- var systemRootClass = this.getClassByName("DTiPhoneSimulatorSystemRoot");
- var roots = systemRootClass("knownRoots");
- var count = roots("count");
-
- var sdks: ISdk[] = [];
- for(var index=0; index < count; index++) {
- var root = roots("objectAtIndex", index);
-
- var displayName = root("sdkDisplayName").toString();
- var version = root("sdkVersion").toString();
- var rootPath = root("sdkRootPath").toString();
-
- sdks.push(new Sdk(displayName, version, rootPath));
- }
-
- return sdks;
- }
-
- private getSdkRootPathByVersion(version: string): string {
- var sdks = this.getInstalledSdks();
- var sdk = _.find(sdks, sdk => sdk.version === version);
- if(!sdk) {
- errors.fail("Unable to find installed sdk with version %s. Verify that you have specified correct version and the sdk with that version is installed.", version);
- }
-
- return sdk.rootPath;
+ return simulator;
+ }).future()();
}
}
-class Sdk implements ISdk {
- constructor(public displayName: string,
- public version: string,
- public rootPath: string) { }
-
- public sdkInfo(): string {
- return [util.format(" Display Name: %s", this.displayName),
- util.format(" Version: %s", this.version),
- util.format(" Root path: %s", this.rootPath)].join(os.EOL);
- }
-}
\ No newline at end of file
diff --git a/lib/simctl.ts b/lib/simctl.ts
new file mode 100644
index 0000000..0b79805
--- /dev/null
+++ b/lib/simctl.ts
@@ -0,0 +1,115 @@
+///
+"use strict";
+
+import childProcess = require("./child-process");
+import future = require("fibers/future");
+import errors = require("./errors");
+import options = require("./options");
+
+export class Simctl implements ISimctl {
+ private devices: IDevice[] = null;
+
+ public launch(deviceId: string, applicationIdentifier: string): IFuture {
+ let args: string[] = [];
+ if(options.waitForDebugger) {
+ args.push("-w");
+ }
+
+ args = args.concat([deviceId, applicationIdentifier]);
+
+ return this.simctlExec("launch", args);
+ }
+
+ public install(deviceId: string, applicationPath: string): IFuture {
+ return this.simctlExec("install", [deviceId, applicationPath]);
+ }
+
+ public uninstall(deviceId: string, applicationIdentifier: string): IFuture {
+ return this.simctlExec("uninstall", [deviceId, applicationIdentifier]);
+ }
+
+ public notifyPost(deviceId: string, notification: string): IFuture {
+ return this.simctlExec("notify_post", [deviceId, notification]);
+ }
+
+ public getDevices(): IFuture {
+ return (() => {
+ if(!this.devices) {
+ this.devices = this.getDevicesCore().wait();
+ }
+
+ return this.devices;
+ }).future()();
+ }
+
+ private getDevicesCore(): IFuture {
+ return (() => {
+ let rawDevices = this.simctlExec("list", ["devices"]).wait();
+
+ // expect to get a listing like
+ // -- iOS 8.1 --
+ // iPhone 4s (3CA6E7DD-220E-45E5-B716-1E992B3A429C) (Shutdown)
+ // ...
+ // -- iOS 8.2 --
+ // iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown)
+ // ...
+ // so, get the `-- iOS X.X --` line to find the sdk (X.X)
+ // and the rest of the listing in order to later find the devices
+
+ let deviceSectionRegex = /-- (iOS|watchOS) (.+) --(\n .+)*/mg;
+ let match = deviceSectionRegex.exec(rawDevices);
+
+ let matches: any[] = [];
+
+ // make an entry for each sdk version
+ while (match !== null) {
+ matches.push(match);
+ match = deviceSectionRegex.exec(rawDevices);
+ }
+
+ if (matches.length < 1) {
+ errors.fail('Could not find device section. ' + match);
+ }
+
+ // get all the devices for each sdk
+ let devices: IDevice[] = [];
+ for (match of matches) {
+ let sdk:string = match[2];
+
+ // split the full match into lines and remove the first
+ for (let line of match[0].split('\n').slice(1)) {
+ // a line is something like
+ // iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown)
+ // retrieve:
+ // iPhone 4s
+ // A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E
+ // Shutdown
+ let lineRegex = /^ ([^\(]+) \(([^\)]+)\) \(([^\)]+)\)( \(([^\)]+)\))*/;
+ let lineMatch = lineRegex.exec(line);
+ if (lineMatch === null) {
+ errors.fail('Could not match line. ' + line);
+ }
+
+ let available = lineMatch[4];
+ if(available === null || available === undefined) {
+ devices.push({
+ name: lineMatch[1],
+ id: lineMatch[2],
+ fullId: "com.apple.CoreSimulator.SimDeviceType." + lineMatch[1],
+ runtimeVersion: sdk,
+ state: lineMatch[3]
+ });
+ }
+ }
+ }
+
+ return devices;
+
+ }).future()();
+ }
+
+ private simctlExec(command: string, args: string[]): IFuture {
+ args = ["xcrun", "simctl", command, ...args];
+ return childProcess.exec(args.join(" "));
+ }
+}
\ No newline at end of file
diff --git a/lib/utils.ts b/lib/utils.ts
index 39c034b..9396113 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,4 +1,15 @@
+///
+"use strict";
+
+import * as Fiber from "fibers";
+
export function stringify(arr: string[], delimiter?: string): string {
delimiter = delimiter || ", ";
return arr.join(delimiter);
+}
+
+export function sleep(ms: number): void {
+ let fiber = Fiber.current;
+ setTimeout(() => fiber.run(), ms);
+ Fiber.yield();
}
\ No newline at end of file
diff --git a/lib/xcode.ts b/lib/xcode.ts
new file mode 100644
index 0000000..3d56fbe
--- /dev/null
+++ b/lib/xcode.ts
@@ -0,0 +1,21 @@
+///
+"use strict";
+
+import childProcess = require("./child-process");
+
+export function getPathFromXcodeSelect(): IFuture {
+ return childProcess.spawn("xcode-select", ["-print-path"]);
+}
+
+export function getXcodeVersionData(): IFuture {
+ return (() => {
+ let rawData = childProcess.spawn("xcodebuild", ["-version"]).wait();
+ let lines = rawData.split("\n");
+ let parts = lines[0].split(" ")[1].split(".");
+ return {
+ major: parts[0],
+ minor: parts[1],
+ build: lines[1].split("Build version ")[1]
+ }
+ }).future()();
+}
\ No newline at end of file