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