From b79f33e4747e879de9819862d7205ac4c0f7ecde Mon Sep 17 00:00:00 2001 From: TsvetanMilanov Date: Fri, 24 Mar 2017 13:28:03 +0200 Subject: [PATCH 1/2] Add debug method to the public API --- lib/bootstrap.ts | 1 + lib/commands/debug.ts | 38 ++-- lib/commands/run.ts | 3 +- lib/common | 2 +- lib/constants.ts | 1 + lib/declarations.d.ts | 6 +- lib/definitions/debug.d.ts | 33 +++- lib/definitions/platform.d.ts | 2 +- .../ios/socket-proxy-factory.ts | 28 ++- lib/nativescript-cli-lib-bootstrap.ts | 1 + lib/services/android-debug-service.ts | 168 +++++++----------- lib/services/debug-data-service.ts | 43 +++++ lib/services/debug-service-base.ts | 25 +++ lib/services/debug-service.ts | 65 +++++++ lib/services/ios-debug-service.ts | 161 +++++++++-------- lib/services/platform-service.ts | 8 +- lib/services/test-execution-service.ts | 51 ++++-- test/debug.ts | 3 + test/stubs.ts | 4 +- 19 files changed, 412 insertions(+), 231 deletions(-) create mode 100644 lib/services/debug-data-service.ts create mode 100644 lib/services/debug-service-base.ts create mode 100644 lib/services/debug-service.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 9b2f649c9f..5c7a73533d 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -21,6 +21,7 @@ $injector.require("tnsModulesService", "./services/tns-modules-service"); $injector.require("platformsData", "./platforms-data"); $injector.require("platformService", "./services/platform-service"); +$injector.require("debugDataService", "./services/debug-data-service"); $injector.require("iOSDebugService", "./services/ios-debug-service"); $injector.require("androidDebugService", "./services/android-debug-service"); diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 0043fc15af..afb97e39c0 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,21 +1,25 @@ -export class DebugPlatformCommand implements ICommand { +import { EOL } from "os"; + +export abstract class DebugPlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private debugService: IDebugService, + constructor(private debugService: IPlatformDebugService, private $devicesService: Mobile.IDevicesService, private $injector: IInjector, private $logger: ILogger, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $config: IConfiguration, private $usbLiveSyncService: ILiveSyncService, + private $debugDataService: IDebugDataService, protected $platformService: IPlatformService, protected $projectData: IProjectData, protected $options: IOptions, protected $platformsData: IPlatformsData) { - this.$projectData.initializeProjectData(); - } + this.$projectData.initializeProjectData(this.$options.path); + } public async execute(args: string[]): Promise { + const debugOptions = this.$options; const deployOptions: IDeployPlatformOptions = { clean: this.$options.clean, device: this.$options.device, @@ -29,8 +33,12 @@ const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); + const debugData = this.$debugDataService.createDebugData(this.debugService, this.$options, buildConfig); + + await this.$platformService.trackProjectType(this.$projectData); + if (this.$options.start) { - return this.debugService.debug(this.$projectData, buildConfig); + return this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); } const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; @@ -49,8 +57,9 @@ await deviceAppData.device.applicationManager.stopApplication(applicationId); - await this.debugService.debug(this.$projectData, buildConfig); + this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); }; + return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, this.$projectData, applicationReloadAction); } @@ -70,22 +79,29 @@ return true; } + + private printDebugInformation(information: string[]): void { + _.each(information, i => { + this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${i}${EOL}`.cyan); + }); + } } export class DebugIOSCommand extends DebugPlatformCommand { - constructor($iOSDebugService: IDebugService, + constructor($iOSDebugService: IPlatformDebugService, $devicesService: Mobile.IDevicesService, $injector: IInjector, $logger: ILogger, $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, $usbLiveSyncService: ILiveSyncService, + $debugDataService: IDebugDataService, $platformService: IPlatformService, $options: IOptions, $projectData: IProjectData, $platformsData: IPlatformsData, $iosDeviceOperations: IIOSDeviceOperations) { - super($iOSDebugService, $devicesService, $injector, $logger, $devicePlatformsConstants, $config, $usbLiveSyncService, $platformService, $projectData, $options, $platformsData); + super($iOSDebugService, $devicesService, $injector, $logger, $devicePlatformsConstants, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData); $iosDeviceOperations.setShouldDispose(this.$options.justlaunch); } @@ -97,19 +113,19 @@ export class DebugIOSCommand extends DebugPlatformCommand { $injector.registerCommand("debug|ios", DebugIOSCommand); export class DebugAndroidCommand extends DebugPlatformCommand { - constructor($androidDebugService: IDebugService, + constructor($androidDebugService: IPlatformDebugService, $devicesService: Mobile.IDevicesService, $injector: IInjector, $logger: ILogger, $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $config: IConfiguration, $usbLiveSyncService: ILiveSyncService, + $debugDataService: IDebugDataService, $platformService: IPlatformService, $options: IOptions, $projectData: IProjectData, $platformsData: IPlatformsData) { - - super($androidDebugService, $devicesService, $injector, $logger, $devicePlatformsConstants, $config, $usbLiveSyncService, $platformService, $projectData, $options, $platformsData); + super($androidDebugService, $devicesService, $injector, $logger, $devicePlatformsConstants, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData); } public async canExecute(args: string[]): Promise { diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 087abf72be..753c90e7fa 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -38,7 +38,8 @@ export class RunCommandBase { justlaunch: this.$options.justlaunch, }; - return this.$platformService.startApplication(args[0], deployOpts, this.$projectData); + await this.$platformService.startApplication(args[0], deployOpts, this.$projectData.projectId); + return this.$platformService.trackProjectType(this.$projectData); } return this.$usbLiveSyncService.liveSync(args[0], this.$projectData); diff --git a/lib/common b/lib/common index 714df2e944..9f80334a2c 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 714df2e944bcb431bc9258b67a5abd89f17a12d6 +Subproject commit 9f80334a2cbd3444c821583739480d31ff8e61b6 diff --git a/lib/constants.ts b/lib/constants.ts index c3fa4e4904..21a4488697 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -69,4 +69,5 @@ export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesCl export const ANGULAR_NAME = "angular"; export const TYPESCRIPT_NAME = "typescript"; export const BUILD_OUTPUT_EVENT_NAME = "buildOutput"; +export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; export const VERSION_STRING = "version"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 69bbb54362..e048ed66ae 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -287,9 +287,9 @@ interface IAndroidToolsInfoData { generateTypings: boolean; } -interface ISocketProxyFactory { - createTCPSocketProxy(factory: () => any): any; - createWebSocketProxy(factory: () => Promise): any; +interface ISocketProxyFactory extends NodeJS.EventEmitter { + createTCPSocketProxy(factory: () => Promise): any; + createWebSocketProxy(factory: () => Promise): Promise; } interface IiOSNotification { diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index e424594783..9da884109f 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -1,6 +1,31 @@ -interface IDebugService { - debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise; - debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise; +interface IDebugData { + deviceIdentifier: string; + applicationIdentifier: string; + pathToAppPackage: string; + projectName?: string; + projectDir?: string; +} + +interface IDebugOptions { + chrome?: boolean; + start?: boolean; + stop?: boolean; + emulator?: boolean; + debugBrk?: boolean; + client?: boolean; + justlaunch?: boolean; +} + +interface IDebugDataService { + createDebugData(debugService: IPlatformDebugService, options: IOptions, buildConfig: IBuildConfig): IDebugData; +} + +interface IDebugService extends NodeJS.EventEmitter { + debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; +} + +interface IPlatformDebugService extends IDebugService { + debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise; debugStop(): Promise platform: string; -} \ No newline at end of file +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 6d9e80b471..8f5e22fc8d 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -118,7 +118,7 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {IProjectData} projectData DTO with information about the project. * @returns {void} */ - startApplication(platform: string, runOptions: IRunPlatformOptions, projectData: IProjectData): Promise; + startApplication(platform: string, runOptions: IRunPlatformOptions, projectId: string): Promise; /** * The emulate command. In addition to `run --emulator` command, it handles the `--available-devices` option to show the available devices. diff --git a/lib/device-sockets/ios/socket-proxy-factory.ts b/lib/device-sockets/ios/socket-proxy-factory.ts index 192ba86511..3f8c6376b5 100644 --- a/lib/device-sockets/ios/socket-proxy-factory.ts +++ b/lib/device-sockets/ios/socket-proxy-factory.ts @@ -1,14 +1,20 @@ +import { EventEmitter } from "events"; +import { CONNECTION_ERROR_EVENT_NAME } from "../../constants"; import { PacketStream } from "./packet-stream"; +import { getAvailablePort } from "../../common/helpers"; import * as net from "net"; import * as ws from "ws"; import temp = require("temp"); -export class SocketProxyFactory implements ISocketProxyFactory { +export class SocketProxyFactory extends EventEmitter implements ISocketProxyFactory { constructor(private $logger: ILogger, + private $errors: IErrors, private $config: IConfiguration, - private $options: IOptions) { } + private $options: IOptions) { + super(); + } - public createTCPSocketProxy(factory: () => Promise): any { + public createTCPSocketProxy(factory: () => Promise): net.Server { this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); let server = net.createServer({ @@ -25,7 +31,7 @@ export class SocketProxyFactory implements ISocketProxyFactory { } }); - const backendSocket: net.Socket = await factory(); + const backendSocket = await factory(); this.$logger.info("Backend socket created."); backendSocket.on("end", () => { @@ -62,9 +68,9 @@ export class SocketProxyFactory implements ISocketProxyFactory { return server; } - public createWebSocketProxy(factory: () => Promise): ws.Server { + public async createWebSocketProxy(factory: () => Promise): Promise { // NOTE: We will try to provide command line options to select ports, at least on the localhost. - let localPort = 8080; + const localPort = await getAvailablePort(8080); this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); @@ -77,7 +83,15 @@ export class SocketProxyFactory implements ISocketProxyFactory { port: localPort, verifyClient: async (info: any, callback: Function) => { this.$logger.info("Frontend client connected."); - const _socket = await factory(); + let _socket; + try { + _socket = await factory(); + } catch (err) { + this.$logger.trace(err); + this.emit(CONNECTION_ERROR_EVENT_NAME, err); + this.$errors.failWithoutHelp("Cannot connect to device socket."); + } + this.$logger.info("Backend socket created."); info.req["__deviceSocket"] = _socket; callback(true); diff --git a/lib/nativescript-cli-lib-bootstrap.ts b/lib/nativescript-cli-lib-bootstrap.ts index 06ba75c84f..505c1a76fc 100644 --- a/lib/nativescript-cli-lib-bootstrap.ts +++ b/lib/nativescript-cli-lib-bootstrap.ts @@ -9,6 +9,7 @@ $injector.requirePublic("companionAppsService", "./common/appbuilder/services/li $injector.requirePublicClass("deviceEmitter", "./common/appbuilder/device-emitter"); $injector.requirePublicClass("deviceLogProvider", "./common/appbuilder/device-log-provider"); $injector.requirePublicClass("localBuildService", "./services/local-build-service"); +$injector.requirePublicClass("debugService", "./services/debug-service"); $injector.require("iOSLogFilter", "./common/mobile/ios/ios-log-filter"); // We need this because some services check if (!$options.justlaunch) to start the device log after some operation. diff --git a/lib/services/android-debug-service.ts b/lib/services/android-debug-service.ts index 24ba6c0e1a..b26c4de4bb 100644 --- a/lib/services/android-debug-service.ts +++ b/lib/services/android-debug-service.ts @@ -1,21 +1,11 @@ -import * as net from "net"; -import * as os from "os"; -import { sleep } from "../common/helpers"; +import { sleep, getAvailablePort } from "../common/helpers"; import { ChildProcess } from "child_process"; +import { DebugServiceBase } from "./debug-service-base"; -class AndroidDebugService implements IDebugService { +class AndroidDebugService extends DebugServiceBase implements IPlatformDebugService { private _device: Mobile.IAndroidDevice = null; private _debuggerClientProcess: ChildProcess; - constructor(private $devicesService: Mobile.IDevicesService, - private $platformService: IPlatformService, - private $platformsData: IPlatformsData, - private $logger: ILogger, - private $options: IOptions, - private $errors: IErrors, - private $config: IConfiguration, - private $androidDeviceDiscovery: Mobile.IDeviceDiscovery) { } - public get platform() { return "android"; } @@ -28,51 +18,42 @@ class AndroidDebugService implements IDebugService { this._device = newDevice; } - public async debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise { - return this.$options.emulator - ? this.debugOnEmulator(projectData, buildConfig) - : this.debugOnDevice(projectData, buildConfig); + constructor(private $devicesService: Mobile.IDevicesService, + private $errors: IErrors, + private $logger: ILogger, + private $config: IConfiguration, + private $androidDeviceDiscovery: Mobile.IDeviceDiscovery, + private $androidProcessService: Mobile.IAndroidProcessService) { + super(); } - private async debugOnEmulator(projectData: IProjectData, buildConfig: IBuildConfig): Promise { - // Assure we've detected the emulator as device - // For example in case deployOnEmulator had stated new emulator instance - // we need some time to detect it. Let's force detection. - await this.$androidDeviceDiscovery.startLookingForDevices(); - await this.debugOnDevice(projectData, buildConfig); + public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + return debugOptions.emulator + ? this.debugOnEmulator(debugData, debugOptions) + : this.debugOnDevice(debugData, debugOptions); } - private isPortAvailable(candidatePort: number): Promise { - return new Promise((resolve, reject) => { - let isResolved = false; - let server = net.createServer(); - - server.on("error", (err: Error) => { - if (!isResolved) { - isResolved = true; - resolve(false); - } - }); - - server.once("close", () => { - if (!isResolved) { // "close" will be emitted right after "error" - isResolved = true; - resolve(true); - } - }); - - server.on("listening", (err: Error) => { - if (err && !isResolved) { - isResolved = true; - resolve(false); - } + public async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); + let action = (device: Mobile.IAndroidDevice): Promise => { + this.device = device; + return this.debugStartCore(debugData.applicationIdentifier, debugOptions); + }; - server.close(); - }); + await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + } - server.listen(candidatePort, "localhost"); + public async debugStop(): Promise { + this.stopDebuggerClient(); + return; + } - }); + private async debugOnEmulator(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + // Assure we've detected the emulator as device + // For example in case deployOnEmulator had stated new emulator instance + // we need some time to detect it. Let's force detection. + await this.$androidDeviceDiscovery.startLookingForDevices(); + return this.debugOnDevice(debugData, debugOptions); } private async getForwardedLocalDebugPortForPackageName(deviceId: string, packageName: string): Promise { @@ -88,15 +69,7 @@ class AndroidDebugService implements IDebugService { if (match) { port = parseInt(match[1]); } else { - let candidatePort = 40000; - - for (; ! await this.isPortAvailable(candidatePort); ++candidatePort) { - if (candidatePort > 65534) { - this.$errors.failWithoutHelp("Unable to find free local port."); - } - } - - port = candidatePort; + port = await getAvailablePort(40000); await this.unixSocketForward(port, `${unixSocketName}`); } @@ -108,38 +81,36 @@ class AndroidDebugService implements IDebugService { return this.device.adb.executeCommand(["forward", `tcp:${local}`, `localabstract:${remote}`]); } - private async debugOnDevice(projectData: IProjectData, buildConfig: IBuildConfig): Promise { + private async debugOnDevice(debugData: IDebugData, debugOptions: IDebugOptions): Promise { let packageFile = ""; - if (!this.$options.start && !this.$options.emulator) { - let cachedDeviceOption = this.$options.forDevice; - - this.$options.forDevice = !!cachedDeviceOption; - - let platformData = this.$platformsData.getPlatformData(this.platform, projectData); - packageFile = this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + if (!debugOptions.start && !debugOptions.emulator) { + packageFile = debugData.pathToAppPackage; this.$logger.out("Using ", packageFile); } - await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }); + await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); + + let action = (device: Mobile.IAndroidDevice): Promise => this.debugCore(device, packageFile, debugData.applicationIdentifier, debugOptions); - let action = (device: Mobile.IAndroidDevice): Promise => this.debugCore(device, packageFile, projectData.projectId); + const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - await this.$devicesService.execute(action); + return _.map(result, r => r.result); } - private async debugCore(device: Mobile.IAndroidDevice, packageFile: string, packageName: string): Promise { + private async debugCore(device: Mobile.IAndroidDevice, packageFile: string, packageName: string, debugOptions: IDebugOptions): Promise { this.device = device; await this.printDebugPort(device.deviceInfo.identifier, packageName); - if (this.$options.start) { - await this.attachDebugger(device.deviceInfo.identifier, packageName); - } else if (this.$options.stop) { + if (debugOptions.start) { + return await this.attachDebugger(device.deviceInfo.identifier, packageName, debugOptions); + } else if (debugOptions.stop) { await this.detachDebugger(packageName); + return null; } else { - await this.startAppWithDebugger(packageFile, packageName); - await this.attachDebugger(device.deviceInfo.identifier, packageName); + await this.startAppWithDebugger(packageFile, packageName, debugOptions); + return await this.attachDebugger(device.deviceInfo.identifier, packageName, debugOptions); } } @@ -148,13 +119,17 @@ class AndroidDebugService implements IDebugService { this.$logger.info("device: " + deviceId + " debug port: " + port + "\n"); } - private async attachDebugger(deviceId: string, packageName: string): Promise { + private async attachDebugger(deviceId: string, packageName: string, debugOptions: IDebugOptions): Promise { + if (!(await this.isAppRunning(packageName, deviceId))) { + this.$errors.failWithoutHelp(`The application ${packageName} does not appear to be running on ${deviceId} or is not built with debugging enabled.`); + } + let startDebuggerCommand = ["am", "broadcast", "-a", `\"${packageName}-debug\"`, "--ez", "enable", "true"]; await this.device.adb.executeShellCommand(startDebuggerCommand); - if (this.$options.client) { + if (debugOptions.client) { let port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); - this.startDebuggerClient(port); + return `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${port}`; } } @@ -162,37 +137,22 @@ class AndroidDebugService implements IDebugService { return this.device.adb.executeShellCommand(["am", "broadcast", "-a", `${packageName}-debug`, "--ez", "enable", "false"]); } - private async startAppWithDebugger(packageFile: string, packageName: string): Promise { - if (!this.$options.emulator && !this.$config.debugLivesync) { + private async startAppWithDebugger(packageFile: string, packageName: string, debugOptions: IDebugOptions): Promise { + if (!debugOptions.emulator && !this.$config.debugLivesync) { await this.device.applicationManager.uninstallApplication(packageName); await this.device.applicationManager.installApplication(packageFile); } - await this.debugStartCore(packageName); + await this.debugStartCore(packageName, debugOptions); } - public async debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise { - await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }); - let action = (device: Mobile.IAndroidDevice): Promise => { - this.device = device; - return this.debugStartCore(projectData.projectId); - }; - - await this.$devicesService.execute(action); - } - - public async debugStop(): Promise { - this.stopDebuggerClient(); - return; - } - - private async debugStartCore(packageName: string): Promise { + private async debugStartCore(packageName: string, debugOptions: IDebugOptions): Promise { // Arguments passed to executeShellCommand must be in array ([]), but it turned out adb shell "arg with intervals" still works correctly. // As we need to redirect output of a command on the device, keep using only one argument. // We could rewrite this with two calls - touch and rm -f , but -f flag is not available on old Android, so rm call will fail when file does not exist. await this.device.applicationManager.stopApplication(packageName); - if (this.$options.debugBrk) { + if (debugOptions.debugBrk) { await this.device.adb.executeShellCommand([`cat /dev/null > /data/local/tmp/${packageName}-debugbreak`]); } @@ -226,8 +186,10 @@ class AndroidDebugService implements IDebugService { } } - private startDebuggerClient(port: Number): void { - this.$logger.info(`To start debugging, open the following URL in Chrome:${os.EOL}chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${port}${os.EOL}`.cyan); + private async isAppRunning(appIdentifier: string, deviceIdentifier: string): Promise { + const debuggableApps = await this.$androidProcessService.getDebuggableApps(deviceIdentifier); + + return !!_.find(debuggableApps, a => a.appIdentifier === appIdentifier); } private stopDebuggerClient(): void { @@ -236,6 +198,6 @@ class AndroidDebugService implements IDebugService { this._debuggerClientProcess = null; } } - } + $injector.register("androidDebugService", AndroidDebugService); diff --git a/lib/services/debug-data-service.ts b/lib/services/debug-data-service.ts new file mode 100644 index 0000000000..3381cfebf3 --- /dev/null +++ b/lib/services/debug-data-service.ts @@ -0,0 +1,43 @@ +export class DebugDataService implements IDebugDataService { + constructor(private $projectData: IProjectData, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $platformService: IPlatformService, + private $platformsData: IPlatformsData) { } + + public createDebugData(debugService: IPlatformDebugService, options: IOptions, buildConfig: IBuildConfig): IDebugData { + this.$projectData.initializeProjectData(options.path); + return { + applicationIdentifier: this.$projectData.projectId, + projectDir: this.$projectData.projectDir, + deviceIdentifier: options.device, + pathToAppPackage: this.getPathToAppPackage(debugService, options, buildConfig), + projectName: this.$projectData.projectName + }; + } + + private getPathToAppPackage(debugService: IPlatformDebugService, options: IOptions, buildConfig: IBuildConfig): string { + if (debugService.platform === this.$devicePlatformsConstants.Android) { + if (!options.start && !options.emulator) { + const platformData = this.getPlatformData(debugService); + + return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + } else { + return null; + } + } else if (debugService.platform === this.$devicePlatformsConstants.iOS) { + if (options.emulator) { + const platformData = this.getPlatformData(debugService); + + return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; + } else { + return null; + } + } + } + + protected getPlatformData(debugService: IPlatformDebugService): IPlatformData { + return this.$platformsData.getPlatformData(debugService.platform, this.$projectData); + } +} + +$injector.register("debugDataService", DebugDataService); diff --git a/lib/services/debug-service-base.ts b/lib/services/debug-service-base.ts new file mode 100644 index 0000000000..5f38dba152 --- /dev/null +++ b/lib/services/debug-service-base.ts @@ -0,0 +1,25 @@ +import { EventEmitter } from "events"; + +export abstract class DebugServiceBase extends EventEmitter implements IPlatformDebugService { + constructor() { + super(); + } + + public abstract get platform(): string; + + public abstract async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; + + public abstract async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise; + + public abstract async debugStop(): Promise; + + protected getCanExecuteAction(deviceIdentifier: string): (device: Mobile.IDevice) => boolean { + return (device: Mobile.IDevice): boolean => { + if (deviceIdentifier) { + return device.deviceInfo.identifier === deviceIdentifier; + } else { + return true; + } + }; + } +} diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts new file mode 100644 index 0000000000..9e3074f563 --- /dev/null +++ b/lib/services/debug-service.ts @@ -0,0 +1,65 @@ +import { platform } from "os"; +import { EventEmitter } from "events"; +import { CONNECTION_ERROR_EVENT_NAME } from "../constants"; + +class DebugService extends EventEmitter { + constructor(private $devicesService: Mobile.IDevicesService, + private $androidDebugService: IPlatformDebugService, + private $iOSDebugService: IPlatformDebugService, + private $errors: IErrors, + private $hostInfo: IHostInfo, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { + super(); + } + + public async debug(debugData: IDebugData, options: IDebugOptions): Promise { + const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); + const debugService = this.getDebugService(device); + + debugService.on(CONNECTION_ERROR_EVENT_NAME, (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e)); + + if (!device) { + this.$errors.failWithoutHelp(`Can't find device with identifier ${debugData.deviceIdentifier}`); + } + + const debugOptions: IDebugOptions = _.merge({}, options || {}); + debugOptions.start = true; + + // TODO: Check if app is running. + const isAppRunning = true; + let result: string[]; + if (device.deviceInfo.platform === this.$devicePlatformsConstants.iOS) { + debugOptions.chrome = true; + if (device.isEmulator && !debugData.pathToAppPackage) { + this.$errors.failWithoutHelp("To debug on iOS simulator you need to provide path to the app package."); + } + + if (this.$hostInfo.isWindows) { + if (!isAppRunning) { + this.$errors.failWithoutHelp(`Application ${debugData.applicationIdentifier} is not running. To be able to debug the application on Windows you must run it.`); + } + + debugOptions.emulator = false; + } else if (!this.$hostInfo.isDarwin) { + this.$errors.failWithoutHelp(`Debugging on iOS devices is not supported for ${platform()} yet.`); + } + + result = await debugService.debug(debugData, debugOptions); + } else if (device.deviceInfo.platform === this.$devicePlatformsConstants.Android) { + debugOptions.client = true; + result = await debugService.debug(debugData, debugOptions); + } + + return _.first(result); + } + + private getDebugService(device: Mobile.IDevice): IPlatformDebugService { + if (device.deviceInfo.platform === this.$devicePlatformsConstants.iOS) { + return this.$iOSDebugService; + } else if (device.deviceInfo.platform === this.$devicePlatformsConstants.Android) { + return this.$androidDebugService; + } + } +} + +$injector.register("debugService", DebugService); diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 6ef796bec8..0ae145be58 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -2,8 +2,10 @@ import * as iOSDevice from "../common/mobile/ios/device/ios-device"; import * as net from "net"; import * as path from "path"; import * as log4js from "log4js"; -import * as os from "os"; import { ChildProcess } from "child_process"; +import { DebugServiceBase } from "./debug-service-base"; +import { CONNECTION_ERROR_EVENT_NAME } from "../constants"; + import byline = require("byline"); const inspectorBackendPort = 18181; @@ -12,7 +14,7 @@ const inspectorNpmPackageName = "tns-ios-inspector"; const inspectorUiDir = "WebInspectorUI/"; const TIMEOUT_SECONDS = 9; -class IOSDebugService implements IDebugService { +class IOSDebugService extends DebugServiceBase implements IPlatformDebugService { private _lldbProcess: ChildProcess; private _sockets: net.Socket[] = []; private _childProcess: ChildProcess; @@ -21,55 +23,55 @@ class IOSDebugService implements IDebugService { constructor(private $platformService: IPlatformService, private $iOSEmulatorServices: Mobile.IEmulatorPlatformServices, private $devicesService: Mobile.IDevicesService, - private $platformsData: IPlatformsData, private $childProcess: IChildProcess, private $logger: ILogger, private $errors: IErrors, private $npmInstallationManager: INpmInstallationManager, - private $options: IOptions, - private $utils: IUtils, private $iOSNotification: IiOSNotification, private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, private $processService: IProcessService, private $socketProxyFactory: ISocketProxyFactory) { + super(); this.$processService.attachToProcessExitSignals(this, this.debugStop); + this.$socketProxyFactory.on(CONNECTION_ERROR_EVENT_NAME, (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e)); } public get platform(): string { return "ios"; } - public async debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise { - if (this.$options.debugBrk && this.$options.start) { + public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + if (debugOptions.debugBrk && debugOptions.start) { this.$errors.failWithoutHelp("Expected exactly one of the --debug-brk or --start options."); } if (this.$devicesService.isOnlyiOSSimultorRunning() || this.$devicesService.deviceCount === 0) { - this.$options.emulator = true; + debugOptions.emulator = true; } - if (this.$options.emulator) { - if (this.$options.debugBrk) { - return this.emulatorDebugBrk(projectData, buildConfig, true); - } else if (this.$options.start) { - return this.emulatorStart(projectData); + if (debugOptions.emulator) { + if (debugOptions.debugBrk) { + return [await this.emulatorDebugBrk(debugData, true, debugOptions)]; + } else if (debugOptions.start) { + return [await this.emulatorStart(debugData, debugOptions)]; } else { - return this.emulatorDebugBrk(projectData, buildConfig); + return [await this.emulatorDebugBrk(debugData, false, debugOptions)]; } } else { - if (this.$options.debugBrk) { - return this.deviceDebugBrk(projectData, buildConfig, true); - } else if (this.$options.start) { - return this.deviceStart(projectData); + if (debugOptions.debugBrk) { + return this.deviceDebugBrk(debugData, true, debugOptions); + } else if (debugOptions.start) { + return this.deviceStart(debugData, debugOptions); } else { - return this.deviceDebugBrk(projectData, buildConfig, false); + return this.deviceDebugBrk(debugData, false, debugOptions); } } } - public async debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise { - await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }); - this.$devicesService.execute(async (device: Mobile.IiOSDevice) => await device.isEmulator ? this.emulatorDebugBrk(projectData, buildConfig) : this.debugBrkCore(device, projectData)); + public async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); + const action = async (device: Mobile.IiOSDevice) => await device.isEmulator ? this.emulatorDebugBrk(debugData, false, debugOptions) : this.debugBrkCore(device, debugData, debugOptions); + await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); } public async debugStop(): Promise { @@ -93,15 +95,13 @@ class IOSDebugService implements IDebugService { } } - private async emulatorDebugBrk(projectData: IProjectData, buildConfig: IBuildConfig, shouldBreak?: boolean): Promise { - let platformData = this.$platformsData.getPlatformData(this.platform, projectData); - - let emulatorPackage = this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig); - + private async emulatorDebugBrk(debugData: IDebugData, shouldBreak: boolean, debugOptions: IDebugOptions): Promise { let args = shouldBreak ? "--nativescript-debug-brk" : "--nativescript-debug-start"; - let child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(emulatorPackage.packageName, { - waitForDebugger: true, captureStdin: true, - args: args, appId: projectData.projectId, + let child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, { + waitForDebugger: true, + captureStdin: true, + args: args, + appId: debugData.applicationIdentifier, skipInstall: true }); @@ -110,8 +110,8 @@ class IOSDebugService implements IDebugService { lineStream.on('data', (line: NodeBuffer) => { let lineText = line.toString(); - if (lineText && _.startsWith(lineText, projectData.projectId)) { - let pid = _.trimStart(lineText, projectData.projectId + ": "); + if (lineText && _.startsWith(lineText, debugData.applicationIdentifier)) { + let pid = _.trimStart(lineText, debugData.applicationIdentifier + ": "); this._lldbProcess = this.$childProcess.spawn("lldb", ["-p", pid]); if (log4js.levels.TRACE.isGreaterThanOrEqualTo(this.$logger.getLevel())) { this._lldbProcess.stdout.pipe(process.stdout); @@ -123,86 +123,99 @@ class IOSDebugService implements IDebugService { } }); - await this.wireDebuggerClient(projectData); + return this.wireDebuggerClient(debugData, debugOptions); } - private async emulatorStart(projectData: IProjectData): Promise { - await this.wireDebuggerClient(projectData); + private async emulatorStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + const result = await this.wireDebuggerClient(debugData, debugOptions); - let attachRequestMessage = this.$iOSNotification.getAttachRequest(projectData.projectId); + let attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier); let iOSEmulator = this.$iOSEmulatorServices; await iOSEmulator.postDarwinNotification(attachRequestMessage); + return result; } - private async deviceDebugBrk(projectData: IProjectData, buildConfig: IBuildConfig, shouldBreak?: boolean): Promise { - await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }); - this.$devicesService.execute(async (device: iOSDevice.IOSDevice) => { + private async deviceDebugBrk(debugData: IDebugData, shouldBreak: boolean, debugOptions: IDebugOptions): Promise { + await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); + const action = async (device: iOSDevice.IOSDevice) => { if (device.isEmulator) { - return await this.emulatorDebugBrk(projectData, buildConfig, shouldBreak); + return await this.emulatorDebugBrk(debugData, shouldBreak, debugOptions); } const runOptions: IRunPlatformOptions = { - device: this.$options.device, - emulator: this.$options.emulator, - justlaunch: this.$options.justlaunch + device: debugData.deviceIdentifier, + emulator: debugOptions.emulator, + justlaunch: debugOptions.justlaunch }; // we intentionally do not wait on this here, because if we did, we'd miss the AppLaunching notification - let action = this.$platformService.startApplication(this.platform, runOptions, projectData); + let startApplicationAction = this.$platformService.startApplication(this.platform, runOptions, debugData.applicationIdentifier); - await this.debugBrkCore(device, projectData, shouldBreak); + const result = await this.debugBrkCore(device, debugData, debugOptions, shouldBreak); - await action; - }); - } + await startApplicationAction; - private async debugBrkCore(device: Mobile.IiOSDevice, projectData: IProjectData, shouldBreak?: boolean): Promise { - let timeout = this.$utils.getMilliSecondsTimeout(TIMEOUT_SECONDS); - await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, timeout, timeout, projectData.projectId, shouldBreak); - await this.wireDebuggerClient(projectData, device); + return result; + }; + + const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(result, r => r.result); } - private async deviceStart(projectData: IProjectData): Promise { - await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device }); - this.$devicesService.execute(async (device: Mobile.IiOSDevice) => device.isEmulator ? await this.emulatorStart(projectData) : await this.deviceStartCore(device, projectData)); + private async debugBrkCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions, shouldBreak?: boolean): Promise { + await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, TIMEOUT_SECONDS, TIMEOUT_SECONDS, debugData.applicationIdentifier, shouldBreak); + return this.wireDebuggerClient(debugData, debugOptions, device); } - private async deviceStartCore(device: Mobile.IiOSDevice, projectData: IProjectData): Promise { - let timeout = this.$utils.getMilliSecondsTimeout(TIMEOUT_SECONDS); - await this.$iOSSocketRequestExecutor.executeAttachRequest(device, timeout, projectData.projectId); - await this.wireDebuggerClient(projectData, device); + private async deviceStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); + const action = async (device: Mobile.IiOSDevice) => device.isEmulator ? await this.emulatorStart(debugData, debugOptions) : await this.deviceStartCore(device, debugData, debugOptions); + const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(result, r => r.result); } - private async wireDebuggerClient(projectData: IProjectData, device?: Mobile.IiOSDevice): Promise { - const factory = async () => { - let socket = device ? await device.connectToPort(inspectorBackendPort) : net.connect(inspectorBackendPort); - this._sockets.push(socket); - return socket; - }; + private async deviceStartCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { + await this.$iOSSocketRequestExecutor.executeAttachRequest(device, TIMEOUT_SECONDS, debugData.applicationIdentifier); + return this.wireDebuggerClient(debugData, debugOptions, device); + } - if (this.$options.chrome) { - this._socketProxy = this.$socketProxyFactory.createWebSocketProxy(factory); + private async wireDebuggerClient(debugData: IDebugData, debugOptions: IDebugOptions, device?: Mobile.IiOSDevice): Promise { + if (debugOptions.chrome) { + this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(device)); const commitSHA = "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; // corresponds to 55.0.2883 Chrome version - this.$logger.info(`To start debugging, open the following URL in Chrome:${os.EOL}chrome-devtools://devtools/remote/serve_file/@${commitSHA}/inspector.html?experiments=true&ws=localhost:${this._socketProxy.options.port}${os.EOL}`.cyan); + return `chrome-devtools://devtools/remote/serve_file/@${commitSHA}/inspector.html?experiments=true&ws=localhost:${this._socketProxy.options.port}`; } else { - this._socketProxy = this.$socketProxyFactory.createTCPSocketProxy(factory); - await this.openAppInspector(this._socketProxy.address(), projectData); + this._socketProxy = this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(device)); + await this.openAppInspector(this._socketProxy.address(), debugData, debugOptions); + return null; } } - private async openAppInspector(fileDescriptor: string, projectData: IProjectData): Promise { - if (this.$options.client) { - let inspectorPath = await this.$npmInstallationManager.getInspectorFromCache(inspectorNpmPackageName, projectData.projectDir); + private async openAppInspector(fileDescriptor: string, debugData: IDebugData, debugOptions: IDebugOptions): Promise { + if (debugOptions.client) { + let inspectorPath = await this.$npmInstallationManager.getInspectorFromCache(inspectorNpmPackageName, debugData.projectDir); let inspectorSourceLocation = path.join(inspectorPath, inspectorUiDir, "Main.html"); let inspectorApplicationPath = path.join(inspectorPath, inspectorAppName); - let cmd = `open -a '${inspectorApplicationPath}' --args '${inspectorSourceLocation}' '${projectData.projectName}' '${fileDescriptor}'`; + let cmd = `open -a '${inspectorApplicationPath}' --args '${inspectorSourceLocation}' '${debugData.projectName}' '${fileDescriptor}'`; await this.$childProcess.exec(cmd); } else { this.$logger.info("Suppressing debugging client."); } } + + private getSocketFactory(device?: Mobile.IiOSDevice): () => Promise { + const factory = async () => { + const socket = device ? await device.connectToPort(inspectorBackendPort) : net.connect(inspectorBackendPort); + this._sockets.push(socket); + return socket; + }; + + factory.bind(this); + return factory; + } } + $injector.register("iOSDebugService", IOSDebugService); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 38d43e3a44..38eecb9e54 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -506,13 +506,11 @@ export class PlatformService extends EventEmitter implements IPlatformService { await this.$devicesService.execute(action, this.getCanExecuteAction(platform, deployOptions)); } - public async startApplication(platform: string, runOptions: IRunPlatformOptions, projectData: IProjectData): Promise { - await this.trackProjectType(projectData); - + public async startApplication(platform: string, runOptions: IRunPlatformOptions, projectId: string): Promise { this.$logger.out("Starting..."); let action = async (device: Mobile.IDevice) => { - await device.applicationManager.startApplication(projectData.projectId); + await device.applicationManager.startApplication(projectId); this.$logger.out(`Successfully started on device with identifier '${device.deviceInfo.identifier}'.`); }; @@ -552,7 +550,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { } await this.deployPlatform(platform, appFilesUpdaterOptions, emulateOptions, projectData, platformSpecificData); - return this.startApplication(platform, emulateOptions, projectData); + return this.startApplication(platform, emulateOptions, projectData.projectId); } private getBuildOutputPath(platform: string, platformData: IPlatformData, options: IBuildForDevice): string { diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 099ec6343c..ff0d40e166 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -17,6 +17,7 @@ class TestExecutionService implements ITestExecutionService { private $platformsData: IPlatformsData, private $usbLiveSyncService: ILiveSyncService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $debugDataService: IDebugDataService, private $httpClient: Server.IHttpClient, private $config: IConfiguration, private $logger: ILogger, @@ -24,8 +25,8 @@ class TestExecutionService implements ITestExecutionService { private $options: IOptions, private $pluginsService: IPluginsService, private $errors: IErrors, - private $androidDebugService: IDebugService, - private $iOSDebugService: IDebugService, + private $androidDebugService: IPlatformDebugService, + private $iOSDebugService: IPlatformDebugService, private $devicesService: Mobile.IDevicesService, private $childProcess: IChildProcess) { } @@ -75,8 +76,9 @@ class TestExecutionService implements ITestExecutionService { if (this.$options.debugBrk) { const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); this.$logger.info('Starting debugger...'); - let debugService: IDebugService = this.$injector.resolve(`${platform}DebugService`); - await debugService.debugStart(projectData, buildConfig); + let debugService: IPlatformDebugService = this.$injector.resolve(`${platform}DebugService`); + const debugData: IDebugData = this.$debugDataService.createDebugData(debugService, this.$options, buildConfig); + await debugService.debugStart(debugData, this.$options); } resolve(); } catch (err) { @@ -128,22 +130,33 @@ class TestExecutionService implements ITestExecutionService { this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - projectDir: this.$options.path, - platformTemplate: this.$options.platformTemplate, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId - }; - - const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); - if (this.$options.debugBrk) { - await this.getDebugService(platform).debug(projectData, buildConfig); + const debugService = this.getDebugService(platform); const deployOptions: IDeployPlatformOptions = { + clean: this.$options.clean, + device: this.$options.device, + emulator: this.$options.emulator, + projectDir: this.$options.path, + platformTemplate: this.$options.platformTemplate, + release: this.$options.release, + provision: this.$options.provision, + teamId: this.$options.teamId + }; + + const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); + const debugData = this.$debugDataService.createDebugData(debugService, this.$options, buildConfig); + await debugService.debug(debugData, this.$options); } else { + const deployOptions: IDeployPlatformOptions = { + clean: this.$options.clean, + device: this.$options.device, + emulator: this.$options.emulator, + projectDir: this.$options.path, + platformTemplate: this.$options.platformTemplate, + release: this.$options.release, + provision: this.$options.provision, + teamId: this.$options.teamId + }; + await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); await this.$usbLiveSyncService.liveSync(platform, projectData); } @@ -198,7 +211,7 @@ class TestExecutionService implements ITestExecutionService { return 'module.exports = ' + JSON.stringify(config); } - private getDebugService(platform: string): IDebugService { + private getDebugService(platform: string): IPlatformDebugService { let lowerCasedPlatform = platform.toLowerCase(); if (lowerCasedPlatform === this.$devicePlatformsConstants.iOS.toLowerCase()) { return this.$iOSDebugService; diff --git a/test/debug.ts b/test/debug.ts index 1efdd6d5eb..5476de8801 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -43,6 +43,9 @@ function createTestInjector(): IInjector { testInjector.register("projectTemplatesService", {}); testInjector.register("xmlValidator", {}); testInjector.register("npm", {}); + testInjector.register("debugDataService", { + createDebugData: () => { return {}; } + }); testInjector.register("androidEmulatorServices", {}); testInjector.register("adb", AndroidDebugBridge); testInjector.register("androidDebugBridgeResultHandler", AndroidDebugBridgeResultHandler); diff --git a/test/stubs.ts b/test/stubs.ts index 591c715d05..be91dca62e 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -469,8 +469,8 @@ function unexpected(msg: string): Error { return err; } -export class DebugServiceStub implements IDebugService { - public async debug(): Promise { +export class DebugServiceStub extends EventEmitter implements IPlatformDebugService { + public async debug(): Promise { return; } From 8f79755c1c1ecad335bd9013ea6e731cfc84de89 Mon Sep 17 00:00:00 2001 From: TsvetanMilanov Date: Tue, 4 Apr 2017 14:21:44 +0300 Subject: [PATCH 2/2] Fix PR comments --- lib/commands/debug.ts | 2 +- lib/common | 2 +- .../ios/socket-proxy-factory.ts | 6 ++-- lib/services/android-debug-service.ts | 9 ++--- lib/services/debug-data-service.ts | 16 ++++----- lib/services/debug-service-base.ts | 4 --- lib/services/debug-service.ts | 33 +++++++++++------ lib/services/ios-debug-service.ts | 36 +++++++++---------- lib/services/test-execution-service.ts | 34 +++++++----------- test/debug.ts | 2 +- 10 files changed, 69 insertions(+), 75 deletions(-) diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index afb97e39c0..30e446366c 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -15,7 +15,7 @@ export abstract class DebugPlatformCommand implements ICommand { protected $projectData: IProjectData, protected $options: IOptions, protected $platformsData: IPlatformsData) { - this.$projectData.initializeProjectData(this.$options.path); + this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { diff --git a/lib/common b/lib/common index 9f80334a2c..f30ac23b0f 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 9f80334a2cbd3444c821583739480d31ff8e61b6 +Subproject commit f30ac23b0f8f0e6309157f2ac0c421b19719b43a diff --git a/lib/device-sockets/ios/socket-proxy-factory.ts b/lib/device-sockets/ios/socket-proxy-factory.ts index 3f8c6376b5..fea0e5a41c 100644 --- a/lib/device-sockets/ios/socket-proxy-factory.ts +++ b/lib/device-sockets/ios/socket-proxy-factory.ts @@ -1,7 +1,6 @@ import { EventEmitter } from "events"; import { CONNECTION_ERROR_EVENT_NAME } from "../../constants"; import { PacketStream } from "./packet-stream"; -import { getAvailablePort } from "../../common/helpers"; import * as net from "net"; import * as ws from "ws"; import temp = require("temp"); @@ -10,7 +9,8 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact constructor(private $logger: ILogger, private $errors: IErrors, private $config: IConfiguration, - private $options: IOptions) { + private $options: IOptions, + private $net: INet) { super(); } @@ -70,7 +70,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact public async createWebSocketProxy(factory: () => Promise): Promise { // NOTE: We will try to provide command line options to select ports, at least on the localhost. - const localPort = await getAvailablePort(8080); + const localPort = await this.$net.getAvailablePortInRange(8080); this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); diff --git a/lib/services/android-debug-service.ts b/lib/services/android-debug-service.ts index b26c4de4bb..cb6cb442e0 100644 --- a/lib/services/android-debug-service.ts +++ b/lib/services/android-debug-service.ts @@ -1,4 +1,4 @@ -import { sleep, getAvailablePort } from "../common/helpers"; +import { sleep } from "../common/helpers"; import { ChildProcess } from "child_process"; import { DebugServiceBase } from "./debug-service-base"; @@ -23,7 +23,8 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ private $logger: ILogger, private $config: IConfiguration, private $androidDeviceDiscovery: Mobile.IDeviceDiscovery, - private $androidProcessService: Mobile.IAndroidProcessService) { + private $androidProcessService: Mobile.IAndroidProcessService, + private $net: INet) { super(); } @@ -69,7 +70,7 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ if (match) { port = parseInt(match[1]); } else { - port = await getAvailablePort(40000); + port = await this.$net.getAvailablePortInRange(40000); await this.unixSocketForward(port, `${unixSocketName}`); } @@ -127,7 +128,7 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ let startDebuggerCommand = ["am", "broadcast", "-a", `\"${packageName}-debug\"`, "--ez", "enable", "true"]; await this.device.adb.executeShellCommand(startDebuggerCommand); - if (debugOptions.client) { + if (debugOptions.chrome) { let port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); return `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${port}`; } diff --git a/lib/services/debug-data-service.ts b/lib/services/debug-data-service.ts index 3381cfebf3..ebd9c0a0f3 100644 --- a/lib/services/debug-data-service.ts +++ b/lib/services/debug-data-service.ts @@ -1,8 +1,8 @@ export class DebugDataService implements IDebugDataService { constructor(private $projectData: IProjectData, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $platformService: IPlatformService, - private $platformsData: IPlatformsData) { } + private $platformsData: IPlatformsData, + private $mobileHelper: Mobile.IMobileHelper) { } public createDebugData(debugService: IPlatformDebugService, options: IOptions, buildConfig: IBuildConfig): IDebugData { this.$projectData.initializeProjectData(options.path); @@ -16,26 +16,24 @@ export class DebugDataService implements IDebugDataService { } private getPathToAppPackage(debugService: IPlatformDebugService, options: IOptions, buildConfig: IBuildConfig): string { - if (debugService.platform === this.$devicePlatformsConstants.Android) { + if (this.$mobileHelper.isAndroidPlatform(debugService.platform)) { if (!options.start && !options.emulator) { const platformData = this.getPlatformData(debugService); return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; - } else { - return null; } - } else if (debugService.platform === this.$devicePlatformsConstants.iOS) { + } else if (this.$mobileHelper.isiOSPlatform(debugService.platform)) { if (options.emulator) { const platformData = this.getPlatformData(debugService); return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; - } else { - return null; } } + + return null; } - protected getPlatformData(debugService: IPlatformDebugService): IPlatformData { + private getPlatformData(debugService: IPlatformDebugService): IPlatformData { return this.$platformsData.getPlatformData(debugService.platform, this.$projectData); } } diff --git a/lib/services/debug-service-base.ts b/lib/services/debug-service-base.ts index 5f38dba152..417c080454 100644 --- a/lib/services/debug-service-base.ts +++ b/lib/services/debug-service-base.ts @@ -1,10 +1,6 @@ import { EventEmitter } from "events"; export abstract class DebugServiceBase extends EventEmitter implements IPlatformDebugService { - constructor() { - super(); - } - public abstract get platform(): string; public abstract async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index 9e3074f563..d1195add67 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -2,34 +2,41 @@ import { platform } from "os"; import { EventEmitter } from "events"; import { CONNECTION_ERROR_EVENT_NAME } from "../constants"; +// This service can't implement IDebugService because +// the debug method returns only one result. class DebugService extends EventEmitter { constructor(private $devicesService: Mobile.IDevicesService, private $androidDebugService: IPlatformDebugService, private $iOSDebugService: IPlatformDebugService, private $errors: IErrors, private $hostInfo: IHostInfo, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { + private $mobileHelper: Mobile.IMobileHelper) { super(); + this.attachConnectionErrorHandlers(); } public async debug(debugData: IDebugData, options: IDebugOptions): Promise { const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); const debugService = this.getDebugService(device); - debugService.on(CONNECTION_ERROR_EVENT_NAME, (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e)); - if (!device) { this.$errors.failWithoutHelp(`Can't find device with identifier ${debugData.deviceIdentifier}`); } - const debugOptions: IDebugOptions = _.merge({}, options || {}); + if (!(await device.applicationManager.isApplicationInstalled(debugData.applicationIdentifier))) { + this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); + } + + const debugOptions: IDebugOptions = _.merge({}, options); debugOptions.start = true; // TODO: Check if app is running. + // For now we can only check if app is running on Android. + // After we find a way to check on iOS we should use it here. const isAppRunning = true; let result: string[]; - if (device.deviceInfo.platform === this.$devicePlatformsConstants.iOS) { - debugOptions.chrome = true; + debugOptions.chrome = !debugOptions.client; + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { if (device.isEmulator && !debugData.pathToAppPackage) { this.$errors.failWithoutHelp("To debug on iOS simulator you need to provide path to the app package."); } @@ -45,8 +52,7 @@ class DebugService extends EventEmitter { } result = await debugService.debug(debugData, debugOptions); - } else if (device.deviceInfo.platform === this.$devicePlatformsConstants.Android) { - debugOptions.client = true; + } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { result = await debugService.debug(debugData, debugOptions); } @@ -54,12 +60,19 @@ class DebugService extends EventEmitter { } private getDebugService(device: Mobile.IDevice): IPlatformDebugService { - if (device.deviceInfo.platform === this.$devicePlatformsConstants.iOS) { + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { return this.$iOSDebugService; - } else if (device.deviceInfo.platform === this.$devicePlatformsConstants.Android) { + } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { return this.$androidDebugService; } } + + private attachConnectionErrorHandlers() { + let connectionErrorHandler = (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e); + connectionErrorHandler = connectionErrorHandler.bind(this); + this.$androidDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); + this.$iOSDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); + } } $injector.register("debugService", DebugService); diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 0ae145be58..0dc16e1518 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -50,27 +50,23 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService } if (debugOptions.emulator) { - if (debugOptions.debugBrk) { - return [await this.emulatorDebugBrk(debugData, true, debugOptions)]; - } else if (debugOptions.start) { + if (debugOptions.start) { return [await this.emulatorStart(debugData, debugOptions)]; } else { - return [await this.emulatorDebugBrk(debugData, false, debugOptions)]; + return [await this.emulatorDebugBrk(debugData, debugOptions)]; } } else { - if (debugOptions.debugBrk) { - return this.deviceDebugBrk(debugData, true, debugOptions); - } else if (debugOptions.start) { + if (debugOptions.start) { return this.deviceStart(debugData, debugOptions); } else { - return this.deviceDebugBrk(debugData, false, debugOptions); + return this.deviceDebugBrk(debugData, debugOptions); } } } public async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); - const action = async (device: Mobile.IiOSDevice) => await device.isEmulator ? this.emulatorDebugBrk(debugData, false, debugOptions) : this.debugBrkCore(device, debugData, debugOptions); + const action = async (device: Mobile.IiOSDevice) => device.isEmulator ? await this.emulatorDebugBrk(debugData, debugOptions) : await this.debugBrkCore(device, debugData, debugOptions); await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); } @@ -95,8 +91,8 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService } } - private async emulatorDebugBrk(debugData: IDebugData, shouldBreak: boolean, debugOptions: IDebugOptions): Promise { - let args = shouldBreak ? "--nativescript-debug-brk" : "--nativescript-debug-start"; + private async emulatorDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + let args = debugOptions.debugBrk ? "--nativescript-debug-brk" : "--nativescript-debug-start"; let child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, { waitForDebugger: true, captureStdin: true, @@ -136,11 +132,11 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService return result; } - private async deviceDebugBrk(debugData: IDebugData, shouldBreak: boolean, debugOptions: IDebugOptions): Promise { + private async deviceDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); const action = async (device: iOSDevice.IOSDevice) => { if (device.isEmulator) { - return await this.emulatorDebugBrk(debugData, shouldBreak, debugOptions); + return await this.emulatorDebugBrk(debugData, debugOptions); } const runOptions: IRunPlatformOptions = { @@ -151,27 +147,27 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService // we intentionally do not wait on this here, because if we did, we'd miss the AppLaunching notification let startApplicationAction = this.$platformService.startApplication(this.platform, runOptions, debugData.applicationIdentifier); - const result = await this.debugBrkCore(device, debugData, debugOptions, shouldBreak); + const result = await this.debugBrkCore(device, debugData, debugOptions); await startApplicationAction; return result; }; - const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - return _.map(result, r => r.result); + const results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(results, r => r.result); } - private async debugBrkCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions, shouldBreak?: boolean): Promise { - await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, TIMEOUT_SECONDS, TIMEOUT_SECONDS, debugData.applicationIdentifier, shouldBreak); + private async debugBrkCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { + await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, TIMEOUT_SECONDS, TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions.debugBrk); return this.wireDebuggerClient(debugData, debugOptions, device); } private async deviceStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); const action = async (device: Mobile.IiOSDevice) => device.isEmulator ? await this.emulatorStart(debugData, debugOptions) : await this.deviceStartCore(device, debugData, debugOptions); - const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - return _.map(result, r => r.result); + const results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(results, r => r.result); } private async deviceStartCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index ff0d40e166..446215f5b9 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -130,33 +130,23 @@ class TestExecutionService implements ITestExecutionService { this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } - if (this.$options.debugBrk) { - const debugService = this.getDebugService(platform); const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - projectDir: this.$options.path, - platformTemplate: this.$options.platformTemplate, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId - }; + const deployOptions: IDeployPlatformOptions = { + clean: this.$options.clean, + device: this.$options.device, + emulator: this.$options.emulator, + projectDir: this.$options.path, + platformTemplate: this.$options.platformTemplate, + release: this.$options.release, + provision: this.$options.provision, + teamId: this.$options.teamId + }; + if (this.$options.debugBrk) { + const debugService = this.getDebugService(platform); const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); const debugData = this.$debugDataService.createDebugData(debugService, this.$options, buildConfig); await debugService.debug(debugData, this.$options); } else { - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - projectDir: this.$options.path, - platformTemplate: this.$options.platformTemplate, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId - }; - await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); await this.$usbLiveSyncService.liveSync(platform, projectData); } diff --git a/test/debug.ts b/test/debug.ts index 5476de8801..dc7de422e9 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -44,7 +44,7 @@ function createTestInjector(): IInjector { testInjector.register("xmlValidator", {}); testInjector.register("npm", {}); testInjector.register("debugDataService", { - createDebugData: () => { return {}; } + createDebugData: () => ({}) }); testInjector.register("androidEmulatorServices", {}); testInjector.register("adb", AndroidDebugBridge);