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..30e446366c 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(); + } 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..f30ac23b0f 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 714df2e944bcb431bc9258b67a5abd89f17a12d6 +Subproject commit f30ac23b0f8f0e6309157f2ac0c421b19719b43a 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..fea0e5a41c 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 * 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, + private $net: INet) { + 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 this.$net.getAvailablePortInRange(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..cb6cb442e0 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 { 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,43 @@ 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, + private $net: INet) { + 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 +70,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 this.$net.getAvailablePortInRange(40000); await this.unixSocketForward(port, `${unixSocketName}`); } @@ -108,38 +82,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 +120,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.chrome) { let port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); - this.startDebuggerClient(port); + return `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${port}`; } } @@ -162,37 +138,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 +187,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 +199,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..ebd9c0a0f3 --- /dev/null +++ b/lib/services/debug-data-service.ts @@ -0,0 +1,41 @@ +export class DebugDataService implements IDebugDataService { + constructor(private $projectData: IProjectData, + private $platformService: IPlatformService, + private $platformsData: IPlatformsData, + private $mobileHelper: Mobile.IMobileHelper) { } + + 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 (this.$mobileHelper.isAndroidPlatform(debugService.platform)) { + if (!options.start && !options.emulator) { + const platformData = this.getPlatformData(debugService); + + return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + } + } else if (this.$mobileHelper.isiOSPlatform(debugService.platform)) { + if (options.emulator) { + const platformData = this.getPlatformData(debugService); + + return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; + } + } + + return null; + } + + private 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..417c080454 --- /dev/null +++ b/lib/services/debug-service-base.ts @@ -0,0 +1,21 @@ +import { EventEmitter } from "events"; + +export abstract class DebugServiceBase extends EventEmitter implements IPlatformDebugService { + 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..d1195add67 --- /dev/null +++ b/lib/services/debug-service.ts @@ -0,0 +1,78 @@ +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 $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); + + if (!device) { + this.$errors.failWithoutHelp(`Can't find device with identifier ${debugData.deviceIdentifier}`); + } + + 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[]; + 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."); + } + + 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 (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { + result = await debugService.debug(debugData, debugOptions); + } + + return _.first(result); + } + + private getDebugService(device: Mobile.IDevice): IPlatformDebugService { + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { + return this.$iOSDebugService; + } 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 6ef796bec8..0dc16e1518 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,51 @@ 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.start) { + return [await this.emulatorStart(debugData, debugOptions)]; } else { - return this.emulatorDebugBrk(projectData, buildConfig); + return [await this.emulatorDebugBrk(debugData, debugOptions)]; } } else { - if (this.$options.debugBrk) { - return this.deviceDebugBrk(projectData, buildConfig, true); - } else if (this.$options.start) { - return this.deviceStart(projectData); + if (debugOptions.start) { + return this.deviceStart(debugData, debugOptions); } else { - return this.deviceDebugBrk(projectData, buildConfig, false); + return this.deviceDebugBrk(debugData, 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) => device.isEmulator ? await this.emulatorDebugBrk(debugData, debugOptions) : await this.debugBrkCore(device, debugData, debugOptions); + await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); } public async debugStop(): Promise { @@ -93,15 +91,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); - - 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, + 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, + args: args, + appId: debugData.applicationIdentifier, skipInstall: true }); @@ -110,8 +106,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 +119,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, 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, 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); - 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 results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(results, 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): Promise { + await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, TIMEOUT_SECONDS, TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions.debugBrk); + 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 results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return _.map(results, 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..446215f5b9 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) { @@ -139,10 +141,11 @@ class TestExecutionService implements ITestExecutionService { 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 buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); + const debugData = this.$debugDataService.createDebugData(debugService, this.$options, buildConfig); + await debugService.debug(debugData, this.$options); } else { await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); await this.$usbLiveSyncService.liveSync(platform, projectData); @@ -198,7 +201,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..dc7de422e9 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: () => ({}) + }); 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; }