diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index d55083a12b..edf24f1af4 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -85,3 +85,8 @@ $injector.require("androidToolsInfo", "./android-tools-info"); $injector.require("iosUsbLiveSyncServiceLocator", "./services/usb-livesync-service"); $injector.require("androidUsbLiveSyncServiceLocator", "./services/usb-livesync-service"); $injector.require("sysInfo", "./sys-info"); + +$injector.require("iOSNotificationService", "./services/ios-notification-service"); +$injector.require("socketProxyFactory", "./device-sockets/ios/socket-proxy-factory"); +$injector.require("iOSNotification", "./device-sockets/ios/notification"); +$injector.require("iOSSocketRequestExecutor", "./device-sockets/ios/socket-request-executor"); diff --git a/lib/common b/lib/common index c280ac5a7d..46e271fc22 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit c280ac5a7dfbb1a431741503232d630a387a2d6e +Subproject commit 46e271fc2280d9b45bd027f78f0f411e9b8d474f diff --git a/lib/declarations.ts b/lib/declarations.ts index a7f6ff2cea..e8906ca5df 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -58,9 +58,8 @@ interface IUsbLiveSyncService { liveSync(platform: string): IFuture; } -interface IPlatformSpecificUsbLiveSyncService { - restartApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths?: Mobile.ILocalToDevicePathData[]): IFuture; - beforeLiveSyncAction?(deviceAppData: Mobile.IDeviceAppData): IFuture; +interface IiOSUsbLiveSyncService extends IPlatformSpecificUsbLiveSyncService { + sendPageReloadMessageToSimulator(): IFuture; } interface IOptions extends ICommonOptions { @@ -168,3 +167,26 @@ interface IAndroidToolsInfoData { */ targetSdkVersion: number; } + +interface ISocketProxyFactory { + createSocketProxy(factory: () => any): IFuture; +} + +interface IiOSNotification { + waitForDebug: string; + attachRequest: string; + appLaunching: string; + readyForAttach: string; + attachAvailabilityQuery: string; + alreadyConnected: string; + attachAvailable: string; +} + +interface IiOSNotificationService { + awaitNotification(npc: Mobile.INotificationProxyClient, notification: string, timeout: number): IFuture; +} + +interface IiOSSocketRequestExecutor { + executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture; + executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture; +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index b7903edb8b..b3a068c579 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -18,6 +18,7 @@ interface IPlatformService { getLatestApplicationPackageForDevice(platformData: IPlatformData): IFuture; getLatestApplicationPackageForEmulator(platformData: IPlatformData): IFuture; copyLastOutput(platform: string, targetPath: string, settings: {isForDevice: boolean}): IFuture; + ensurePlatformInstalled(platform: string): IFuture; } interface IPlatformData { diff --git a/lib/device-sockets/ios/notification.ts b/lib/device-sockets/ios/notification.ts new file mode 100644 index 0000000000..53d9e51637 --- /dev/null +++ b/lib/device-sockets/ios/notification.ts @@ -0,0 +1,47 @@ +/// +"use strict"; + +export class IOSNotification implements IiOSNotification { + private static WAIT_FOR_DEBUG_NOTIFICATION_NAME = "WaitForDebugger"; + private static ATTACH_REQUEST_NOTIFICATION_NAME = "AttachRequest"; + private static APP_LAUNCHING_NOTIFICATION_NAME = "AppLaunching"; + private static READY_FOR_ATTACH_NOTIFICATION_NAME = "ReadyForAttach"; + private static ATTACH_AVAILABILITY_QUERY_NOTIFICATION_NAME = "AttachAvailabilityQuery"; + private static ALREADY_CONNECTED_NOTIFICATION_NAME = "AlreadyConnected"; + private static ATTACH_AVAILABLE_NOTIFICATION_NAME = "AttachAvailable"; + + constructor(private $projectData: IProjectData) { } + + public get waitForDebug() { + return this.formatNotification(IOSNotification.WAIT_FOR_DEBUG_NOTIFICATION_NAME); + } + + public get attachRequest(): string { + return this.formatNotification(IOSNotification.ATTACH_REQUEST_NOTIFICATION_NAME); + } + + public get appLaunching(): string { + return this.formatNotification(IOSNotification.APP_LAUNCHING_NOTIFICATION_NAME); + } + + public get readyForAttach(): string { + return this.formatNotification(IOSNotification.READY_FOR_ATTACH_NOTIFICATION_NAME); + } + + public get attachAvailabilityQuery() { + return this.formatNotification(IOSNotification.ATTACH_AVAILABILITY_QUERY_NOTIFICATION_NAME); + } + + public get alreadyConnected() { + return this.formatNotification(IOSNotification.ALREADY_CONNECTED_NOTIFICATION_NAME); + } + + public get attachAvailable() { + return this.formatNotification(IOSNotification.ATTACH_AVAILABLE_NOTIFICATION_NAME); + } + + private formatNotification(notification: string) { + return `${this.$projectData.projectId}:NativeScript.Debug.${notification}`; + } +} +$injector.register("iOSNotification", IOSNotification); diff --git a/lib/device-sockets/ios/packet-stream.ts b/lib/device-sockets/ios/packet-stream.ts new file mode 100644 index 0000000000..fb0d77e8e7 --- /dev/null +++ b/lib/device-sockets/ios/packet-stream.ts @@ -0,0 +1,36 @@ +/// +"use strict"; + +import * as stream from "stream"; + +export class PacketStream extends stream.Transform { + private buffer: Buffer; + private offset: number; + + constructor(opts?: stream.TransformOptions) { + super(opts); + } + + public _transform(packet: any, encoding: string, done: Function): void { + while (packet.length > 0) { + if (!this.buffer) { + // read length + let length = packet.readInt32BE(0); + this.buffer = new Buffer(length); + this.offset = 0; + packet = packet.slice(4); + } + + packet.copy(this.buffer, this.offset); + let copied = Math.min(this.buffer.length - this.offset, packet.length); + this.offset += copied; + packet = packet.slice(copied); + + if (this.offset === this.buffer.length) { + this.push(this.buffer); + this.buffer = undefined; + } + } + done(); + } +} diff --git a/lib/device-sockets/ios/socket-proxy-factory.ts b/lib/device-sockets/ios/socket-proxy-factory.ts new file mode 100644 index 0000000000..91b2dd20e8 --- /dev/null +++ b/lib/device-sockets/ios/socket-proxy-factory.ts @@ -0,0 +1,126 @@ +/// +"use strict"; + +import { PacketStream } from "./packet-stream"; +import * as net from "net"; +import * as semver from "semver"; +import * as ws from "ws"; +import temp = require("temp"); +import * as helpers from "../../common/helpers"; + +export class SocketProxyFactory implements ISocketProxyFactory { + constructor(private $logger: ILogger, + private $projectData: IProjectData, + private $projectDataService: IProjectDataService) { } + + public createSocketProxy(factory: () => net.Socket): IFuture { + return (() => { + let socketFactory = (callback: (_socket: net.Socket) => void) => helpers.connectEventually(factory, callback); + + this.$projectDataService.initialize(this.$projectData.projectDir); + let frameworkVersion = this.$projectDataService.getValue("tns-ios").wait().version; + let result: any; + + if(semver.gte(frameworkVersion, "1.4.0")) { + result = this.createTcpSocketProxy(socketFactory); + } else { + result = this.createWebSocketProxy(socketFactory); + } + + return result; + }).future()(); + } + + private createWebSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): ws.Server { + // NOTE: We will try to provide command line options to select ports, at least on the localhost. + let localPort = 8080; + + this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); + + // NB: When the inspector frontend connects we might not have connected to the inspector backend yet. + // That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect. + // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it + // in the connection callback. + + let server = ws.createServer({ + port: localPort, + verifyClient: (info: any, callback: Function) => { + this.$logger.info("Frontend client connected."); + socketFactory((_socket: any) => { + this.$logger.info("Backend socket created."); + info.req["__deviceSocket"] = _socket; + callback(true); + }); + } + }); + server.on("connection", (webSocket) => { + let encoding = "utf16le"; + + let deviceSocket: net.Socket = (webSocket.upgradeReq)["__deviceSocket"]; + let packets = new PacketStream(); + deviceSocket.pipe(packets); + + packets.on("data", (buffer: Buffer) => { + webSocket.send(buffer.toString(encoding)); + }); + + webSocket.on("message", (message, flags) => { + let length = Buffer.byteLength(message, encoding); + let payload = new Buffer(length + 4); + payload.writeInt32BE(length, 0); + payload.write(message, 4, length, encoding); + deviceSocket.write(payload); + }); + + deviceSocket.on("end", () => { + this.$logger.info("Backend socket closed!"); + process.exit(0); + }); + + webSocket.on("close", () => { + this.$logger.info('Frontend socket closed!'); + process.exit(0); + }); + + }); + + this.$logger.info("Opened localhost " + localPort); + return server; + } + + private createTcpSocketProxy(socketFactory: (handler: (socket: net.Socket) => void) => void): string { + this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); + + let server = net.createServer({ + allowHalfOpen: true + }); + + server.on("connection", (frontendSocket: net.Socket) => { + this.$logger.info("Frontend client connected."); + + frontendSocket.on("end", () => { + this.$logger.info('Frontend socket closed!'); + process.exit(0); + }); + + socketFactory((backendSocket: net.Socket) => { + this.$logger.info("Backend socket created."); + + backendSocket.on("end", () => { + this.$logger.info("Backend socket closed!"); + process.exit(0); + }); + + backendSocket.pipe(frontendSocket); + frontendSocket.pipe(backendSocket); + frontendSocket.resume(); + }); + }); + + let socketFileLocation = temp.path({ suffix: ".sock" }); + server.listen(socketFileLocation); + + return socketFileLocation; + } +} +$injector.register("socketProxyFactory", SocketProxyFactory); diff --git a/lib/device-sockets/ios/socket-request-executor.ts b/lib/device-sockets/ios/socket-request-executor.ts new file mode 100644 index 0000000000..cd27426e22 --- /dev/null +++ b/lib/device-sockets/ios/socket-request-executor.ts @@ -0,0 +1,75 @@ +/// +"use strict"; + +import * as helpers from "../../common/helpers"; +import * as iOSProxyServices from "../../common/mobile/ios/ios-proxy-services"; + +export class IOSSocketRequestExecutor implements IiOSSocketRequestExecutor { + constructor(private $errors: IErrors, + private $injector: IInjector, + private $iOSNotification: IiOSNotification, + private $iOSNotificationService: IiOSNotificationService, + private $logger: ILogger, + private $projectData: IProjectData, + private $socketProxyFactory: ISocketProxyFactory) { } + + public executeAttachRequest(device: Mobile.IiOSDevice, timeout: number): IFuture { + return (() => { + let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector); + + let [alreadyConnected, readyForAttach, attachAvailable] = [this.$iOSNotification.alreadyConnected, this.$iOSNotification.readyForAttach, this.$iOSNotification.attachAvailable] + .map((notification) => this.$iOSNotificationService.awaitNotification(npc, notification, timeout)); + + npc.postNotificationAndAttachForData(this.$iOSNotification.attachAvailabilityQuery); + + let receivedNotification: IFuture; + try { + receivedNotification = helpers.whenAny(alreadyConnected, readyForAttach, attachAvailable).wait(); + } catch (e) { + this.$errors.failWithoutHelp(`The application ${this.$projectData.projectId} does not appear to be running on ${device.deviceInfo.displayName} or is not built with debugging enabled.`); + } + + switch (receivedNotification) { + case alreadyConnected: + this.$errors.failWithoutHelp("A client is already connected."); + break; + case attachAvailable: + this.executeAttachAvailable(npc, timeout).wait(); + break; + case readyForAttach: + break; + } + }).future()(); + } + + public executeLaunchRequest(device: Mobile.IiOSDevice, timeout: number, readyForAttachTimeout: number): IFuture { + return (() => { + let npc = new iOSProxyServices.NotificationProxyClient(device, this.$injector); + + try { + this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.appLaunching, timeout).wait(); + process.nextTick(() => { + npc.postNotificationAndAttachForData(this.$iOSNotification.waitForDebug ); + npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest); + }); + + this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, readyForAttachTimeout).wait(); + } catch(e) { + this.$logger.trace(`Timeout error: ${e}`); + this.$errors.failWithoutHelp("Timeout waiting for response from NativeScript runtime."); + } + }).future()(); + } + + private executeAttachAvailable(npc: Mobile.INotificationProxyClient, timeout: number): IFuture { + return (() => { + process.nextTick(() => npc.postNotificationAndAttachForData(this.$iOSNotification.attachRequest)); + try { + this.$iOSNotificationService.awaitNotification(npc, this.$iOSNotification.readyForAttach, timeout).wait(); + } catch (e) { + this.$errors.failWithoutHelp(`The application ${this.$projectData.projectId} timed out when performing the socket handshake.`); + } + }).future()(); + } +} +$injector.register("iOSSocketRequestExecutor", IOSSocketRequestExecutor); diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 870218e197..ac709a3922 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -1,68 +1,15 @@ /// "use strict"; -import * as iOSProxyServices from "../common/mobile/ios/ios-proxy-services"; import * as iOSDevice from "../common/mobile/ios/ios-device"; + import * as net from "net"; -import * as ws from "ws"; -import * as stream from "stream"; import * as path from "path"; -import Future = require("fibers/future"); import * as semver from "semver"; -import temp = require("temp"); import byline = require("byline"); -module notification { - function formatNotification(bundleId: string, notification: string) { - return `${bundleId}:NativeScript.Debug.${notification}`; - } - - export function waitForDebug(bundleId: string): string { - return formatNotification(bundleId, "WaitForDebugger"); - } - - export function attachRequest(bundleId: string): string { - return formatNotification(bundleId, "AttachRequest"); - } - - export function appLaunching(bundleId: string): string { - return formatNotification(bundleId, "AppLaunching"); - } - - export function readyForAttach(bundleId: string): string { - return formatNotification(bundleId, "ReadyForAttach"); - } - - export function attachAvailabilityQuery(bundleId: string) { - return formatNotification(bundleId, "AttachAvailabilityQuery"); - } - - export function alreadyConnected(bundleId: string) { - return formatNotification(bundleId, "AlreadyConnected"); - } - - export function attachAvailable(bundleId: string) { - return formatNotification(bundleId, "AttachAvailable"); - } -} - let InspectorBackendPort = 18181; -function connectEventually(factory: () => net.Socket, handler: (_socket: net.Socket) => void) { - function tryConnect() { - let tryConnectAfterTimeout = setTimeout.bind(undefined, tryConnect, 1000); - - let socket = factory(); - socket.on("connect", () => { - socket.removeListener("error", tryConnectAfterTimeout); - handler(socket); - }); - socket.on("error", tryConnectAfterTimeout); - } - - tryConnect(); -} - class IOSDebugService implements IDebugService { private static TIMEOUT_SECONDS = 90; @@ -80,7 +27,10 @@ class IOSDebugService implements IDebugService { private $npmInstallationManager: INpmInstallationManager, private $options: IOptions, private $projectDataService: IProjectDataService, - private $utils: IUtils) { } + private $utils: IUtils, + private $iOSNotification: IiOSNotification, + private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, + private $socketProxyFactory: ISocketProxyFactory) { } get platform(): string { return "ios"; @@ -113,6 +63,13 @@ class IOSDebugService implements IDebugService { this.$errors.failWithoutHelp("Failed to select device or emulator to debug on."); } + public debugStart(): IFuture { + return (() => { + this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }).wait(); + this.$devicesService.execute((device: Mobile.IiOSDevice) => this.debugBrkCore(device)).wait(); + }).future()(); + } + private emulatorDebugBrk(): IFuture { return (() => { let platformData = this.$platformsData.getPlatformData(this.platform); @@ -141,8 +98,7 @@ class IOSDebugService implements IDebugService { return (() => { this.wireDebuggerClient(() => net.connect(InspectorBackendPort)).wait(); - let projectId = this.$projectData.projectId; - let attachRequestMessage = notification.attachRequest(projectId); + let attachRequestMessage = this.$iOSNotification.attachRequest; let iOSEmulator = this.$iOSEmulatorServices; iOSEmulator.postDarwinNotification(attachRequestMessage).wait(); @@ -161,92 +117,35 @@ class IOSDebugService implements IDebugService { }).future()(); } - public debugStart(): IFuture { - return (() => { - this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }).wait(); - this.$devicesService.execute((device: iOSDevice.IOSDevice) => this.debugBrkCore(device)).wait(); - }).future()(); - } - - private debugBrkCore(iosDevice: iOSDevice.IOSDevice): IFuture { + private debugBrkCore(device: Mobile.IiOSDevice): IFuture { return (() => { - let projectId = this.$projectData.projectId; - let npc = new iOSProxyServices.NotificationProxyClient(iosDevice, this.$injector); - - try { - let timeout = this.$utils.getMilliSecondsTimeout(IOSDebugService.TIMEOUT_SECONDS); - awaitNotification(npc, notification.appLaunching(projectId), timeout).wait(); - process.nextTick(() => { - npc.postNotificationAndAttachForData(notification.waitForDebug(projectId)); - npc.postNotificationAndAttachForData(notification.attachRequest(projectId)); - }); - - awaitNotification(npc, notification.readyForAttach(projectId), this.getReadyForAttachTimeout(timeout)).wait(); - } catch(e) { - this.$logger.trace(`Timeout error: ${e}`); - this.$errors.failWithoutHelp("Timeout waiting for NativeScript debugger."); - } + let timeout = this.$utils.getMilliSecondsTimeout(IOSDebugService.TIMEOUT_SECONDS); + let readyForAttachTimeout = this.getReadyForAttachTimeout(timeout); - this.wireDebuggerClient(() => iosDevice.connectToPort(InspectorBackendPort)).wait(); + this.$iOSSocketRequestExecutor.executeLaunchRequest(device, timeout, readyForAttachTimeout).wait(); + this.wireDebuggerClient(() => device.connectToPort(InspectorBackendPort)).wait(); }).future()(); } private deviceStart(): IFuture { return (() => { this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }).wait(); - this.$devicesService.execute(device => (() => { - let iosDevice = device; - let projectId = this.$projectData.projectId; - let npc = new iOSProxyServices.NotificationProxyClient(iosDevice, this.$injector); - - let timeout = this.getReadyForAttachTimeout(); - let [alreadyConnected, readyForAttach, attachAvailable] = [ - notification.alreadyConnected(projectId), - notification.readyForAttach(projectId), - notification.attachAvailable(projectId) - ].map((notification) => awaitNotification(npc, notification, timeout)); - - npc.postNotificationAndAttachForData(notification.attachAvailabilityQuery(projectId)); - - let receivedNotification: IFuture; - try { - receivedNotification = whenAny(alreadyConnected, readyForAttach, attachAvailable).wait(); - } catch (e) { - this.$errors.failWithoutHelp(`The application ${projectId} does not appear to be running on ${device.deviceInfo.displayName} or is not built with debugging enabled.`); - } + this.$devicesService.execute((device: Mobile.IiOSDevice) => this.deviceStartCore(device)).wait(); + }).future()(); + } - switch (receivedNotification) { - case alreadyConnected: - this.$errors.failWithoutHelp("A debugger is already connected."); - break; - case attachAvailable: - process.nextTick(() => npc.postNotificationAndAttachForData(notification.attachRequest(projectId))); - try { - awaitNotification(npc, notification.readyForAttach(projectId), timeout).wait(); - } catch (e) { - this.$errors.failWithoutHelp(`The application ${projectId} timed out when performing the NativeScript debugger handshake.`); - } - this.wireDebuggerClient(() => iosDevice.connectToPort(InspectorBackendPort)).wait(); - break; - case readyForAttach: - this.wireDebuggerClient(() => iosDevice.connectToPort(InspectorBackendPort)).wait(); - break; - } - }).future()()).wait(); + private deviceStartCore(device: Mobile.IiOSDevice): IFuture { + return (() => { + let timeout = this.getReadyForAttachTimeout(); + this.$iOSSocketRequestExecutor.executeAttachRequest(device, timeout).wait(); + this.wireDebuggerClient(() => device.connectToPort(InspectorBackendPort)).wait(); }).future()(); } private wireDebuggerClient(factory: () => net.Socket): IFuture { return (() => { - let frameworkVersion = this.getProjectFrameworkVersion().wait(); - let socketFileLocation = ""; - if (semver.gte(frameworkVersion, "1.4.0")) { - socketFileLocation = createTcpSocketProxy(this.$logger, (callback) => connectEventually(factory, callback)); - } else { - createWebSocketProxy(this.$logger, (callback) => connectEventually(factory, callback)); - } - - this.executeOpenDebuggerClient(socketFileLocation).wait(); + let socketProxy = this.$socketProxyFactory.createSocketProxy(factory).wait(); + this.executeOpenDebuggerClient(socketProxy).wait(); }).future()(); } @@ -314,170 +213,3 @@ class IOSDebugService implements IDebugService { } } $injector.register("iOSDebugService", IOSDebugService); - -function createTcpSocketProxy($logger: ILogger, socketFactory: (handler: (socket: net.Socket) => void) => void): string { - $logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); - - let server = net.createServer({ - allowHalfOpen: true - }); - - server.on("connection", (frontendSocket: net.Socket) => { - $logger.info("Frontend client connected."); - - frontendSocket.on("end", function() { - $logger.info('Frontend socket closed!'); - process.exit(0); - }); - - socketFactory((backendSocket) => { - $logger.info("Backend socket created."); - - backendSocket.on("end", () => { - $logger.info("Backend socket closed!"); - process.exit(0); - }); - - backendSocket.pipe(frontendSocket); - frontendSocket.pipe(backendSocket); - frontendSocket.resume(); - }); - }); - - let socketFileLocation = temp.path({ suffix: ".sock" }); - server.listen(socketFileLocation); - - return socketFileLocation; -} - -function createWebSocketProxy($logger: ILogger, socketFactory: (handler: (socket: net.Socket) => void) => void): ws.Server { - // NOTE: We will try to provide command line options to select ports, at least on the localhost. - let localPort = 8080; - - $logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); - - // NB: When the inspector frontend connects we might not have connected to the inspector backend yet. - // That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect. - // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it - // in the connection callback. - - let server = ws.createServer({ - port: localPort, - verifyClient: (info: any, callback: any) => { - $logger.info("Frontend client connected."); - socketFactory((_socket: any) => { - $logger.info("Backend socket created."); - info.req["__deviceSocket"] = _socket; - callback(true); - }); - } - }); - server.on("connection", (webSocket) => { - let deviceSocket: net.Socket = (webSocket.upgradeReq)["__deviceSocket"]; - let packets = new PacketStream(); - deviceSocket.pipe(packets); - - packets.on("data", (buffer: Buffer) => { - webSocket.send(buffer.toString("utf16le")); - }); - - webSocket.on("message", (message, flags) => { - let length = Buffer.byteLength(message, "utf16le"); - let payload = new Buffer(length + 4); - payload.writeInt32BE(length, 0); - payload.write(message, 4, length, "utf16le"); - deviceSocket.write(payload); - }); - - deviceSocket.on("end", () => { - $logger.info("Backend socket closed!"); - process.exit(0); - }); - - webSocket.on("close", () => { - $logger.info('Frontend socket closed!'); - process.exit(0); - }); - }); - - $logger.info("Opened localhost " + localPort); - return server; -} - -function awaitNotification(npc: iOSProxyServices.NotificationProxyClient, notification: string, timeout: number): IFuture { - let future = new Future(); - - let timeoutObject = setTimeout(() => { - detachObserver(); - future.throw(new Error(`Timeout receiving ${notification} notification.`)); - }, timeout); - - function notificationObserver(_notification: string) { - clearTimeout(timeoutObject); - detachObserver(); - future.return(_notification); - } - - function detachObserver() { - process.nextTick(() => npc.removeObserver(notification, notificationObserver)); - } - - npc.addObserver(notification, notificationObserver); - - return future; -} - -function whenAny(...futures: IFuture[]): IFuture> { - let resultFuture = new Future>(); - let futuresLeft = futures.length; - let futureLocal: IFuture; - - for (let future of futures) { - futureLocal = future; - future.resolve((error, result?) => { - futuresLeft--; - - if (!resultFuture.isResolved()) { - if (typeof error === "undefined") { - resultFuture.return(futureLocal); - } else if (futuresLeft === 0) { - resultFuture.throw(new Error("None of the futures succeeded.")); - } - } - }); - } - - return resultFuture; -} - -class PacketStream extends stream.Transform { - private buffer: Buffer; - private offset: number; - - constructor(opts?: stream.TransformOptions) { - super(opts); - } - - public _transform(packet: any, encoding: string, done: Function): void { - while (packet.length > 0) { - if (!this.buffer) { - // read length - let length = packet.readInt32BE(0); - this.buffer = new Buffer(length); - this.offset = 0; - packet = packet.slice(4); - } - - packet.copy(this.buffer, this.offset); - let copied = Math.min(this.buffer.length - this.offset, packet.length); - this.offset += copied; - packet = packet.slice(copied); - - if (this.offset === this.buffer.length) { - this.push(this.buffer); - this.buffer = undefined; - } - } - done(); - } -} diff --git a/lib/services/ios-notification-service.ts b/lib/services/ios-notification-service.ts new file mode 100644 index 0000000000..437d7d08f5 --- /dev/null +++ b/lib/services/ios-notification-service.ts @@ -0,0 +1,30 @@ +/// +"use strict"; + +import Future = require("fibers/future"); + +export class IOSNotificationService implements IiOSNotificationService { + public awaitNotification(npc: Mobile.INotificationProxyClient, notification: string, timeout: number): IFuture { + let future = new Future(); + + let timeoutToken = setTimeout(() => { + detachObserver(); + future.throw(new Error(`Timeout receiving ${notification} notification.`)); + }, timeout); + + function notificationObserver(_notification: string) { + clearTimeout(timeoutToken); + detachObserver(); + future.return(_notification); + } + + function detachObserver() { + process.nextTick(() => npc.removeObserver(notification, notificationObserver)); + } + + npc.addObserver(notification, notificationObserver); + + return future; + } +} +$injector.register("iOSNotificationService", IOSNotificationService); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 8c71bfa609..daf7447617 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -417,7 +417,7 @@ export class PlatformService implements IPlatformService { }).future()(); } - private ensurePlatformInstalled(platform: string): IFuture { + public ensurePlatformInstalled(platform: string): IFuture { return (() => { if(!this.isPlatformInstalled(platform).wait()) { this.addPlatform(platform).wait(); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 23ecfba066..8cdf587fc9 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -67,20 +67,18 @@ class TestExecutionService implements ITestExecutionService { } }; - let notInstalledAppOnDeviceAction = (device: Mobile.IDevice): IFuture => { + let notInstalledAppOnDeviceAction = (device: Mobile.IDevice): IFuture => { return (() => { this.$platformService.installOnDevice(platform).wait(); this.detourEntryPoint(projectFilesPath).wait(); - return true; - }).future()(); + }).future()(); }; - let notRunningiOSSimulatorAction = (): IFuture => { + let notRunningiOSSimulatorAction = (): IFuture => { return (() => { this.$platformService.deployOnEmulator(this.$devicePlatformsConstants.iOS.toLowerCase()).wait(); this.detourEntryPoint(projectFilesPath).wait(); - return true; - }).future()(); + }).future()(); }; let beforeBatchLiveSyncAction = (filePath: string): IFuture => { @@ -91,17 +89,23 @@ class TestExecutionService implements ITestExecutionService { }; let localProjectRootPath = platform.toLowerCase() === "ios" ? platformData.appDestinationDirectoryPath : null; - this.$usbLiveSyncServiceBase.sync(platform, - this.$projectData.projectId, - projectFilesPath, - constants.LIVESYNC_EXCLUDED_DIRECTORIES, - watchGlob, - platformSpecificLiveSyncServices, - notInstalledAppOnDeviceAction, - notRunningiOSSimulatorAction, - localProjectRootPath, - (device: Mobile.IDevice, deviceAppData:Mobile.IDeviceAppData) => Future.fromResult(), - beforeBatchLiveSyncAction).wait(); + + let liveSyncData = { + platform: platform, + appIdentifier: this.$projectData.projectId, + projectFilesPath: projectFilesPath, + excludedProjectDirsAndFiles: constants.LIVESYNC_EXCLUDED_DIRECTORIES, + watchGlob: watchGlob, + platformSpecificLiveSyncServices: platformSpecificLiveSyncServices, + notInstalledAppOnDeviceAction: notInstalledAppOnDeviceAction, + notRunningiOSSimulatorAction: notRunningiOSSimulatorAction, + localProjectRootPath: localProjectRootPath, + beforeBatchLiveSyncAction: beforeBatchLiveSyncAction, + shouldRestartApplication: (localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Future.fromResult(!this.$options.debugBrk), + canExecuteFastLiveSync: (filePath: string) => false, + }; + + this.$usbLiveSyncServiceBase.sync(liveSyncData).wait(); if (this.$options.debugBrk) { this.$logger.info('Starting debugger...'); diff --git a/lib/services/usb-livesync-service.ts b/lib/services/usb-livesync-service.ts index f4b657565a..88ed619ea5 100644 --- a/lib/services/usb-livesync-service.ts +++ b/lib/services/usb-livesync-service.ts @@ -6,9 +6,12 @@ import * as constants from "../constants"; import * as usbLivesyncServiceBaseLib from "../common/services/usb-livesync-service-base"; import * as path from "path"; import * as semver from "semver"; +import * as net from "net"; import Future = require("fibers/future"); +import * as helpers from "../common/helpers"; export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncServiceBase implements IUsbLiveSyncService { + private excludedProjectDirsAndFiles = [ "**/*.ts", ]; @@ -41,6 +44,9 @@ export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncSer return (() => { platform = platform || this.initialize(platform).wait(); let platformLowerCase = platform ? platform.toLowerCase() : null; + + this.$platformService.ensurePlatformInstalled(platform).wait(); + let platformData = this.$platformsData.getPlatformData(platformLowerCase); if (platformLowerCase === this.$devicePlatformsConstants.Android.toLowerCase()) { @@ -62,19 +68,9 @@ export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncSer let projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - let notInstalledAppOnDeviceAction = (device: Mobile.IDevice): IFuture => { - return (() => { - this.$platformService.deployOnDevice(platform).wait(); - return false; - }).future()(); - }; + let notInstalledAppOnDeviceAction = (device: Mobile.IDevice): IFuture => this.$platformService.installOnDevice(platform); - let notRunningiOSSimulatorAction = (): IFuture => { - return (() => { - this.$platformService.deployOnEmulator(this.$devicePlatformsConstants.iOS.toLowerCase()).wait(); - return false; - }).future()(); - }; + let notRunningiOSSimulatorAction = (): IFuture => this.$platformService.deployOnEmulator(this.$devicePlatformsConstants.iOS.toLowerCase()); let beforeLiveSyncAction = (device: Mobile.IDevice, deviceAppData: Mobile.IDeviceAppData): IFuture => { let platformSpecificUsbLiveSyncService = this.resolveUsbLiveSyncService(platform || this.$devicesService.platform, device); @@ -87,21 +83,22 @@ export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncSer let beforeBatchLiveSyncAction = (filePath: string): IFuture => { return (() => { let projectFileInfo = this.getProjectFileInfo(filePath); - let result = path.join(projectFilesPath, path.relative(path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), projectFileInfo.onDeviceName)); + let mappedFilePath = path.join(projectFilesPath, path.relative(path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), projectFileInfo.onDeviceName)); // Handle files that are in App_Resources/ - if(filePath.indexOf(path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, platformData.normalizedPlatformName)) > -1) { - let appResourcesRelativePath = path.relative(path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, platformData.normalizedPlatformName), filePath); - result = path.join(platformData.platformProjectService.getAppResourcesDestinationDirectoryPath().wait(), appResourcesRelativePath); - } - - if(filePath.indexOf(path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME)) > -1 && - filePath.indexOf(path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, platformData.normalizedPlatformName)) === -1) { + let appResourcesDirectoryPath = path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); + let platformSpecificAppResourcesDirectoryPath = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); + if(filePath.indexOf(appResourcesDirectoryPath) > -1 && filePath.indexOf(platformSpecificAppResourcesDirectoryPath) === -1) { this.$logger.warn(`Unable to sync ${filePath}.`); return null; } + if(filePath.indexOf(platformSpecificAppResourcesDirectoryPath) > -1) { + let appResourcesRelativePath = path.relative(path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, + platformData.normalizedPlatformName), filePath); + mappedFilePath = path.join(platformData.platformProjectService.getAppResourcesDestinationDirectoryPath().wait(), appResourcesRelativePath); + } - return result; + return mappedFilePath; }).future()(); }; @@ -118,25 +115,59 @@ export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncSer let localProjectRootPath = platform.toLowerCase() === "ios" ? platformData.appDestinationDirectoryPath : null; - this.sync(platform, - this.$projectData.projectId, - projectFilesPath, - this.excludedProjectDirsAndFiles, - watchGlob, - platformSpecificLiveSyncServices, - notInstalledAppOnDeviceAction, - notRunningiOSSimulatorAction, - localProjectRootPath, - beforeLiveSyncAction, - beforeBatchLiveSyncAction, - iOSSimulatorRelativeToProjectBasePathAction - ).wait(); + let fastLivesyncFileExtensions = [".css", ".xml"]; + + let fastLiveSync = (filePath: string) => { + this.$dispatcher.dispatch(() => { + return (() => { + this.$platformService.preparePlatform(platform).wait(); + let mappedFilePath = beforeBatchLiveSyncAction(filePath).wait(); + + if (this.shouldSynciOSSimulator(platform).wait()) { + this.$iOSEmulatorServices.transferFiles(this.$projectData.projectId, [filePath], iOSSimulatorRelativeToProjectBasePathAction).wait(); + + let platformSpecificUsbLiveSyncService = this.resolvePlatformSpecificLiveSyncService(platform || this.$devicesService.platform, null, platformSpecificLiveSyncServices); + platformSpecificUsbLiveSyncService.sendPageReloadMessageToSimulator().wait(); + } else { + let deviceAppData = this.$deviceAppDataFactory.create(this.$projectData.projectId, this.$mobileHelper.normalizePlatformName(platform)); + let localToDevicePaths = this.createLocalToDevicePaths(platform, this.$projectData.projectId, localProjectRootPath || projectFilesPath, [mappedFilePath]); + + let devices = this.$devicesService.getDeviceInstances(); + _.each(devices, (device: Mobile.IDevice) => { + this.transferFiles(device, deviceAppData, localToDevicePaths, projectFilesPath, true).wait(); + let platformSpecificUsbLiveSyncService = this.resolvePlatformSpecificLiveSyncService(platform || this.$devicesService.platform, device, platformSpecificLiveSyncServices); + return platformSpecificUsbLiveSyncService.sendPageReloadMessageToDevice(deviceAppData).wait(); + }); + } + }).future()(); + }); + }; + + let liveSyncData = { + platform: platform, + appIdentifier: this.$projectData.projectId, + projectFilesPath: projectFilesPath, + excludedProjectDirsAndFiles: this.excludedProjectDirsAndFiles, + watchGlob: watchGlob, + platformSpecificLiveSyncServices: platformSpecificLiveSyncServices, + notInstalledAppOnDeviceAction: notInstalledAppOnDeviceAction, + notRunningiOSSimulatorAction: notRunningiOSSimulatorAction, + localProjectRootPath: localProjectRootPath, + beforeLiveSyncAction: beforeLiveSyncAction, + beforeBatchLiveSyncAction: beforeBatchLiveSyncAction, + iOSSimulatorRelativeToProjectBasePathAction: iOSSimulatorRelativeToProjectBasePathAction, + canExecuteFastLiveSync: (filePath: string) => _.contains(fastLivesyncFileExtensions, path.extname(filePath)), + fastLiveSync: fastLiveSync + }; + + this.sync(liveSyncData).wait(); }).future()(); } protected preparePlatformForSync(platform: string) { this.$platformService.preparePlatform(platform).wait(); } + private resolveUsbLiveSyncService(platform: string, device: Mobile.IDevice): IPlatformSpecificUsbLiveSyncService { let platformSpecificUsbLiveSyncService: IPlatformSpecificUsbLiveSyncService = null; if(platform.toLowerCase() === "android") { @@ -150,8 +181,16 @@ export class UsbLiveSyncService extends usbLivesyncServiceBaseLib.UsbLiveSyncSer } $injector.register("usbLiveSyncService", UsbLiveSyncService); -export class IOSUsbLiveSyncService implements IPlatformSpecificUsbLiveSyncService { - constructor(private _device: Mobile.IDevice) { } +let currentPageReloadId = 0; + +export class IOSUsbLiveSyncService implements IiOSUsbLiveSyncService { + private static BACKEND_PORT = 18181; + + constructor(private _device: Mobile.IDevice, + private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, + private $iOSNotification: IiOSNotification, + private $iOSEmulatorServices: Mobile.IiOSSimulatorService, + private $injector: IInjector) { } private get device(): Mobile.IiOSDevice { return this._device; @@ -160,22 +199,53 @@ export class IOSUsbLiveSyncService implements IPlatformSpecificUsbLiveSyncServic public restartApplication(deviceAppData: Mobile.IDeviceAppData): IFuture { return this.device.applicationManager.restartApplication(deviceAppData.appIdentifier); } + + public sendPageReloadMessageToDevice(deviceAppData: Mobile.IDeviceAppData): IFuture { + return (() => { + let timeout = 9000; + this.$iOSSocketRequestExecutor.executeAttachRequest(this.device, timeout).wait(); + let socket = this.device.connectToPort(IOSUsbLiveSyncService.BACKEND_PORT); + this.sendPageReloadMessage(socket); + }).future()(); + } + + public sendPageReloadMessageToSimulator(): IFuture { + helpers.connectEventually(() => net.connect(IOSUsbLiveSyncService.BACKEND_PORT), (socket: net.Socket) => this.sendPageReloadMessage(socket)); + return this.$iOSEmulatorServices.postDarwinNotification(this.$iOSNotification.attachRequest); + } + + private sendPageReloadMessage(socket: net.Socket): void { + try { + this.sendPageReloadMessageCore(socket); + } finally { + socket.destroy(); + } + } + + private sendPageReloadMessageCore(socket: net.Socket): void { + let message = `{ "method":"Page.reload","params":{"ignoreCache":false},"id":${++currentPageReloadId} }`; + let length = Buffer.byteLength(message, "utf16le"); + let payload = new Buffer(length + 4); + payload.writeInt32BE(length, 0); + payload.write(message, 4, length, "utf16le"); + socket.write(payload); + } } $injector.register("iosUsbLiveSyncServiceLocator", {factory: IOSUsbLiveSyncService}); export class AndroidUsbLiveSyncService extends androidLiveSyncServiceLib.AndroidLiveSyncService implements IPlatformSpecificUsbLiveSyncService { + private static BACKEND_PORT = 18181; + constructor(_device: Mobile.IDevice, $fs: IFileSystem, $mobileHelper: Mobile.IMobileHelper, private $options: IOptions) { super(_device, $fs, $mobileHelper); - } public restartApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): IFuture { return (() => { - this.device.adb.executeShellCommand(["chmod", "777", deviceAppData.deviceProjectRootPath]).wait(); - this.device.adb.executeShellCommand(["chmod", "777", `/data/local/tmp/${deviceAppData.appIdentifier}`]).wait(); + this.device.adb.executeShellCommand(["chmod", "777", deviceAppData.deviceProjectRootPath, `/data/local/tmp/${deviceAppData.appIdentifier}`]).wait(); if(this.$options.companion) { let commands = [ this.liveSyncCommands.SyncFilesCommand() ]; @@ -193,10 +263,35 @@ export class AndroidUsbLiveSyncService extends androidLiveSyncServiceLib.Android public beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): IFuture { return (() => { let deviceRootPath = `/data/local/tmp/${deviceAppData.appIdentifier}`; - this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "fullsync")]).wait(); - this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "sync")]).wait(); - this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync")]).wait(); + this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "fullsync"), + this.$mobileHelper.buildDevicePath(deviceRootPath, "sync"), + this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync")]).wait(); + }).future()(); + } + + public sendPageReloadMessageToDevice(deviceAppData: Mobile.IDeviceAppData): IFuture { + return (() => { + this.device.adb.executeCommand(["forward", `tcp:${AndroidUsbLiveSyncService.BACKEND_PORT.toString()}`, `localabstract:${deviceAppData.appIdentifier}-livesync`]).wait(); + this.sendPageReloadMessage().wait(); }).future()(); } + + private sendPageReloadMessage(): IFuture { + let future = new Future(); + + let socket = new net.Socket(); + socket.connect(AndroidUsbLiveSyncService.BACKEND_PORT, '127.0.0.1', () => { + try { + socket.write(new Buffer([0, 0, 0, 1, 1])); + future.return(); + } catch(e) { + future.throw(e); + } finally { + socket.destroy(); + } + }); + + return future; + } } $injector.register("androidUsbLiveSyncServiceLocator", {factory: AndroidUsbLiveSyncService}); diff --git a/package.json b/package.json index f2a1ad2027..b4a7f45813 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "gulp": "3.9.0", "iconv-lite": "0.4.11", "inquirer": "0.9.0", - "ios-sim-portable": "1.0.13-gamma", + "ios-sim-portable": "1.0.13-delta", "lockfile": "1.0.1", "lodash": "3.10.0", "log4js": "0.6.26",