From 608737f28c013b1b02755116aa8820c37837bc10 Mon Sep 17 00:00:00 2001 From: Dimitar Kerezov Date: Thu, 31 Aug 2017 16:12:33 +0300 Subject: [PATCH] Debug improvements (#3094) * Debug improvements Includes but is not limited to: * `enableDebugging` - each promise is now resolved with an object * added docs for `debuggerAttached` event * added `debuggerDetached` event and docs for it * PR Fixes --- PublicAPI.md | 18 ++++++++++ lib/commands/debug.ts | 3 +- lib/constants.ts | 1 + lib/declarations.d.ts | 11 ++++-- lib/definitions/debug.d.ts | 14 ++++++-- lib/definitions/livesync.d.ts | 14 ++++---- lib/services/debug-service.ts | 15 ++++++-- lib/services/livesync/livesync-service.ts | 43 ++++++++++++----------- test/services/debug-service.ts | 12 ++++--- 9 files changed, 91 insertions(+), 40 deletions(-) diff --git a/PublicAPI.md b/PublicAPI.md index a2056851cc..fd56bf03e9 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -839,6 +839,24 @@ tns.liveSyncService.on("userInteractionNeeded", data => { }); ``` +* debuggerAttached - raised whenever CLI attaches the backend debugging socket and a frontend debugging client may be attached. The event is raised with an object containing the device's identifier: + +Example: +```JavaScript +tns.liveSyncService.on("debuggerAttached", debugInfo => { + console.log(`Backend client connected, frontend client may be connected at ${debugInfo.url}`); +}); +``` + +* debuggerDetached - raised whenever CLI detaches the backend debugging socket. The event is raised with an object of the `IDebugInformation` type: + +Example: +```JavaScript +tns.liveSyncService.on("debuggerDetached", debugInfo => { + console.log(`Detached debugger for device with id ${debugInfo.deviceIdentifier}`); +}); +``` + ## How to add a new method to Public API CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification. For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`. diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 3b5d4ef9d8..28be7548f7 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -31,7 +31,8 @@ export class DebugPlatformCommand implements ICommand { debugData.deviceIdentifier = selectedDeviceForDebug.deviceInfo.identifier; if (this.$options.start) { - return this.$liveSyncService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions)); + await this.$liveSyncService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions)); + return; } await this.$devicesService.detectCurrentlyAttachedDevices({ shouldReturnImmediateResult: false, platform: this.platform }); diff --git a/lib/constants.ts b/lib/constants.ts index 2ce856a677..346d475dab 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -87,6 +87,7 @@ export const BUILD_OUTPUT_EVENT_NAME = "buildOutput"; export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; export const USER_INTERACTION_NEEDED_EVENT_NAME = "userInteractionNeeded"; export const DEBUGGER_ATTACHED_EVENT_NAME = "debuggerAttached"; +export const DEBUGGER_DETACHED_EVENT_NAME = "debuggerDetached"; export const VERSION_STRING = "version"; export const INSPECTOR_CACHE_DIRNAME = "ios-inspector"; export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index f26c55ba52..e91c807a72 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -372,7 +372,15 @@ interface ICreateProjectOptions extends INpmInstallConfigurationOptionsBase { pathToTemplate?: string; } -interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions, INpmInstallConfigurationOptions { +interface IDebugInformation extends IPort { + url: string; +} + +interface IPort { + port: Number; +} + +interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions, INpmInstallConfigurationOptions, IPort { all: boolean; client: boolean; compileSdk: number; @@ -386,7 +394,6 @@ interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator tsc: boolean; ng: boolean; androidTypings: boolean; - port: Number; production: boolean; //npm flag sdk: string; syncAllFiles: boolean; diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index 37640c0848..698185efd8 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -105,9 +105,9 @@ interface IDebugServiceBase extends NodeJS.EventEmitter { * Starts debug operation based on the specified debug data. * @param {IDebugData} debugData Describes information for device and application that will be debugged. * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. - * @returns {Promise} Array of URLs that can be used for debugging or a string representing a single url that can be used for debugging. + * @returns {Promise} Full url and port where the frontend client can be connected. */ - debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; + debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; } interface IDebugService extends IDebugServiceBase { @@ -122,7 +122,7 @@ interface IDebugService extends IDebugServiceBase { /** * Describes actions required for debugging on specific platform (Android or iOS). */ -interface IPlatformDebugService extends IDebugServiceBase, IPlatform { +interface IPlatformDebugService extends IPlatform, NodeJS.EventEmitter { /** * Starts debug operation. * @param {IDebugData} debugData Describes information for device and application that will be debugged. @@ -136,4 +136,12 @@ interface IPlatformDebugService extends IDebugServiceBase, IPlatform { * @returns {Promise} */ debugStop(): Promise; + + /** + * Starts debug operation based on the specified debug data. + * @param {IDebugData} debugData Describes information for device and application that will be debugged. + * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. + * @returns {Promise} Full url where the frontend client may be connected. + */ + debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; } diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index 1ba0230268..19b73bcd11 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -206,18 +206,18 @@ interface ILiveSyncService { interface IDebugLiveSyncService extends ILiveSyncService { /** * Prints debug information. - * @param {string[]} information Array of information to be printed. Note that false-like values will be stripped from the array. - * @returns {void} + * @param {IDebugInformation} debugInformation Information to be printed. + * @returns {IDebugInformation} Full url and port where the frontend client can be connected. */ - printDebugInformation(information: string): void; + printDebugInformation(debugInformation: IDebugInformation): IDebugInformation; /** * Enables debugging for the specified devices * @param {IEnableDebuggingDeviceOptions[]} deviceOpts Settings used for enabling debugging for each device. * @param {IDebuggingAdditionalOptions} enableDebuggingOptions Settings used for enabling debugging. - * @returns {Promise[]} Array of promises for each device. + * @returns {Promise[]} Array of promises for each device. */ - enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], enableDebuggingOptions: IDebuggingAdditionalOptions): Promise[]; + enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], enableDebuggingOptions: IDebuggingAdditionalOptions): Promise[]; /** * Disables debugging for the specified devices @@ -230,9 +230,9 @@ interface IDebugLiveSyncService extends ILiveSyncService { /** * Attaches a debugger to the specified device. * @param {IAttachDebuggerOptions} settings Settings used for controling the attaching process. - * @returns {Promise} + * @returns {Promise} Full url and port where the frontend client can be connected. */ - attachDebugger(settings: IAttachDebuggerOptions): Promise; + attachDebugger(settings: IAttachDebuggerOptions): Promise; } /** diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index ab7151baca..a276e484d4 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -1,4 +1,5 @@ import { platform } from "os"; +import { parse } from "url"; import { EventEmitter } from "events"; import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../constants"; import { CONNECTED_STATUS } from "../common/constants"; @@ -14,7 +15,7 @@ export class DebugService extends EventEmitter implements IDebugService { this._platformDebugServices = {}; } - public async debug(debugData: IDebugData, options: IDebugOptions): Promise { + public async debug(debugData: IDebugData, options: IDebugOptions): Promise { const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); if (!device) { @@ -57,7 +58,7 @@ export class DebugService extends EventEmitter implements IDebugService { result = await debugService.debug(debugData, debugOptions); } - return result; + return this.getDebugInformation(result); } public debugStop(deviceIdentifier: string): Promise { @@ -92,6 +93,16 @@ export class DebugService extends EventEmitter implements IDebugService { connectionErrorHandler = connectionErrorHandler.bind(this); platformDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); } + + private getDebugInformation(fullUrl: string): IDebugInformation { + const parseQueryString = true; + const wsQueryParam = parse(fullUrl, parseQueryString).query.ws; + const hostPortSplit = wsQueryParam && wsQueryParam.split(":"); + return { + url: fullUrl, + port: hostPortSplit && +hostPortSplit[1] + }; + } } $injector.register("debugService", DebugService); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index d01f372d82..e4bb9936c6 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -1,10 +1,9 @@ import * as path from "path"; import * as choki from "chokidar"; -import { parse } from "url"; import { EOL } from "os"; import { EventEmitter } from "events"; import { hook } from "../../common/helpers"; -import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME } from "../../constants"; +import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME, DEBUGGER_DETACHED_EVENT_NAME } from "../../constants"; import { FileExtensions, DeviceTypes } from "../../common/constants"; const deviceDescriptorPrimaryKey = "identifier"; @@ -101,7 +100,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi return currentDescriptors || []; } - private async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string): Promise { + private async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string): Promise { const deviceDescriptor = this.getDeviceDescriptor(liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, projectData.projectDir); return deviceDescriptor && deviceDescriptor.debugggingEnabled ? @@ -138,13 +137,14 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); } - private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise { + private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise { await this.$platformService.trackProjectType(projectData); const deviceAppData = liveSyncResultInfo.deviceAppData; const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier; await this.$debugService.debugStop(deviceIdentifier); + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier }); const applicationId = deviceAppData.appIdentifier; const attachDebuggerOptions: IAttachDebuggerOptions = { @@ -181,7 +181,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, { projectDir: projectData.projectDir }); } - public async attachDebugger(settings: IAttachDebuggerOptions): Promise { + public async attachDebugger(settings: IAttachDebuggerOptions): Promise { // Default values if (settings.debugOptions) { settings.debugOptions.chrome = settings.debugOptions.chrome === undefined ? true : settings.debugOptions.chrome; @@ -208,22 +208,19 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi }; debugData.pathToAppPackage = this.$platformService.lastOutputPath(settings.platform, buildConfig, projectData, settings.outputPath); - this.printDebugInformation(await this.$debugService.debug(debugData, settings.debugOptions)); + return this.printDebugInformation(await this.$debugService.debug(debugData, settings.debugOptions)); } - public printDebugInformation(information: string): void { - if (!!information) { - const wsQueryParam = parse(information).query.ws; - const hostPortSplit = wsQueryParam && wsQueryParam.split(":"); - this.emit(DEBUGGER_ATTACHED_EVENT_NAME, { - url: information, - port: hostPortSplit && hostPortSplit[1] - }); - this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${information}${EOL}`.cyan); + public printDebugInformation(debugInformation: IDebugInformation): IDebugInformation { + if (!!debugInformation.url) { + this.emit(DEBUGGER_ATTACHED_EVENT_NAME, debugInformation); + this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${debugInformation.url}${EOL}`.cyan); } + + return debugInformation; } - public enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { + public enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { return _.map(deviceOpts, d => this.enableDebuggingCore(d, debuggingAdditionalOptions)); } @@ -233,7 +230,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi return _.find(deviceDescriptors, d => d.identifier === deviceIdentifier); } - private async enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + private async enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir); if (!currentDeviceDescriptor) { this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceOption.deviceIdentifier}`); @@ -250,16 +247,19 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi debugOptions: deviceOption.debugOptions }; + let debugInformation: IDebugInformation; try { - await this.attachDebugger(attachDebuggerOptions); + debugInformation = await this.attachDebugger(attachDebuggerOptions); } catch (err) { this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); attachDebuggerOptions.debugOptions.start = false; - await this.attachDebugger(attachDebuggerOptions); + debugInformation = await this.attachDebugger(attachDebuggerOptions); } + + return debugInformation; } - private async enableDebuggingCore(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + private async enableDebuggingCore(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { const liveSyncProcessInfo: ILiveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir]; if (liveSyncProcessInfo && liveSyncProcessInfo.currentSyncAction) { await liveSyncProcessInfo.currentSyncAction; @@ -290,7 +290,8 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}. Could not find device.`); } - return this.$debugService.debugStop(currentDevice.deviceInfo.identifier); + await this.$debugService.debugStop(currentDevice.deviceInfo.identifier); + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: currentDeviceDescriptor.identifier }); } @hook("liveSync") diff --git a/test/services/debug-service.ts b/test/services/debug-service.ts index b320178890..2b4da60510 100644 --- a/test/services/debug-service.ts +++ b/test/services/debug-service.ts @@ -6,7 +6,8 @@ import { EventEmitter } from "events"; import * as constants from "../../lib/common/constants"; import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../../lib/constants"; -const fakeChromeDebugUrl = "fakeChromeDebugUrl"; +const fakeChromeDebugPort = 123; +const fakeChromeDebugUrl = `fakeChromeDebugUrl?experiments=true&ws=localhost:${fakeChromeDebugPort}`; class PlatformDebugService extends EventEmitter /* implements IPlatformDebugService */ { public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { return fakeChromeDebugUrl; @@ -202,7 +203,7 @@ describe("debugService", () => { }); }); - describe("returns chrome url returned by platform specific debug service", () => { + describe("returns chrome url along with port returned by platform specific debug service", () => { _.each(["android", "iOS"], platform => { it(`for ${platform} device`, async () => { const testData = getDefaultTestData(); @@ -212,9 +213,12 @@ describe("debugService", () => { const debugService = testInjector.resolve(DebugService); const debugData = getDebugData(); - const url = await debugService.debug(debugData, null); + const debugInfo = await debugService.debug(debugData, null); - assert.deepEqual(url, fakeChromeDebugUrl); + assert.deepEqual(debugInfo, { + url: fakeChromeDebugUrl, + port: fakeChromeDebugPort + }); }); }); });