import * as os from 'os' ; import { DebugProtocol } from 'vscode-debugprotocol' ; import { LoggingDebugSession , ErrorDestination , Response , logger } from 'vscode-debugadapter' ; import { ChromeDebugAdapter } from './chromeDebugAdapter' ; import { ITargetFilter , ChromeConnection , IChromeError } from './chromeConnection' ; import { BasePathTransformer } from '../transformers/basePathTransformer' ; import { BaseSourceMapTransformer } from '../transformers/baseSourceMapTransformer' ; import { LineColTransformer } from '../transformers/lineNumberTransformer' ; import { IDebugAdapter } from '../debugAdapterInterfaces' ; import { telemetry , ExceptionType , IExecutionResultTelemetryProperties , TelemetryPropertyCollector , ITelemetryPropertyCollector } from '../telemetry' ; import * as utils from '../utils' ; import { ExecutionTimingsReporter , StepProgressEventsEmitter , IObservableEvents , IStepStartedEventsEmitter , IFinishedStartingUpEventsEmitter } from '../executionTimingsReporter' ; import { Breakpoints } from './breakpoints' ; import { ScriptContainer } from '../chrome/scripts' ; export interface IChromeDebugAdapterOpts { targetFilter ? : ITargetFilter ; logFilePath ? : string ; chromeConnection ? : typeof ChromeConnection ; pathTransformer ? : { new ( ) : BasePathTransformer ; } ; sourceMapTransformer ? : { new ( sourceHandles ) : BaseSourceMapTransformer ; } ; lineColTransformer ? : { new ( session ) : LineColTransformer ; } ; breakpoints ? : typeof Breakpoints ; scriptContainer ? : typeof ScriptContainer ; } export interface IChromeDebugSessionOpts extends IChromeDebugAdapterOpts { /** The class of the adapter, which is instantiated for each session */ adapter : typeof ChromeDebugAdapter ; extensionName : string ; } export const ErrorTelemetryEventName = 'error' ; type RequestHandleError = Error | DebugProtocol . Message | IChromeError ; function isMessage ( e ) { return ! ! ( < DebugProtocol . Message > e ) . format ; } function isChromeError ( e ) { return ! ! ( < IChromeError > e ) . data ; } export class ChromeDebugSession extends LoggingDebugSession implements IObservableEvents < IStepStartedEventsEmitter & IFinishedStartingUpEventsEmitter > { private readonly _readyForUserTimeoutInMilliseconds = 5 * 60 * 1000 ; private _debugAdapter : IDebugAdapter & IObservableEvents < IStepStartedEventsEmitter & IFinishedStartingUpEventsEmitter > ; private _extensionName : string ; public readonly events : StepProgressEventsEmitter ; private reporter = new ExecutionTimingsReporter ( ) ; private haveTimingsWhileStartingUpBeenReported = false ; public static readonly FinishedStartingUpEventName = 'finishedStartingUp' ; /**
     * This needs a bit of explanation -
     * The Session is reinstantiated for each session, but consumers need to configure their instance of
     * ChromeDebugSession. Consumers should call getSession with their config options, then call
     * DebugSession.run with the result. Alternatively they could subclass ChromeDebugSession and pass
     * their options to the super constructor, but I think this is easier to follow.
     */ public static getSession ( opts ) { return class extends ChromeDebugSession { constructor ( debuggerLinesAndColumnsStartAt1 ? , isServer ? ) { super ( debuggerLinesAndColumnsStartAt1 , isServer , opts ) ; } } ; } public constructor ( obsolete_debuggerLinesAndColumnsStartAt1 ? , obsolete_isServer ? , opts ? ) { super ( opts . logFilePath , obsolete_debuggerLinesAndColumnsStartAt1 , obsolete_isServer ) ; logVersionInfo ( ) ; this . _extensionName = opts . extensionName ; this . _debugAdapter = new ( < any > opts . adapter ) ( opts , this ) ; this . events = new StepProgressEventsEmitter ( [ this . _debugAdapter . events ] ) ; this . configureExecutionTimingsReporting ( ) ; const safeGetErrDetails = err => { let errMsg ; try { errMsg = ( err && ( < Error > err ) . stack ) ? ( < Error > err ) . stack : JSON . stringify ( err ) ; } catch ( e ) { errMsg = 'Error while handling previous error: ' + e . stack ; } return errMsg ; } ; const reportErrorTelemetry = ( err , exceptionType ) => { let properties = { } ; properties . successful = 'false' ; properties . exceptionType = exceptionType ; utils . fillErrorDetails ( properties , err ) ; telemetry . reportEvent ( ErrorTelemetryEventName , properties ) ; } ; process . addListener ( 'uncaughtException' , ( err ) => { reportErrorTelemetry ( err , 'uncaughtException' ) ; logger . error ( `******** Unhandled error in debug adapter: ${ safeGetErrDetails ( err ) }` ) ; } ) ; process . addListener ( 'unhandledRejection' , ( err ) => { reportErrorTelemetry ( err , 'unhandledRejection' ) ; logger . error ( `******** Unhandled error in debug adapter - Unhandled promise rejection: ${ safeGetErrDetails ( err ) }` ) ; } ) ; } /**
     * Overload dispatchRequest to the debug adapters' Promise-based methods instead of DebugSession's callback-based methods
     */ protected dispatchRequest ( request ) { this . reportTelemetry ( `ClientRequest/${ request . command }` , async ( reportFailure , telemetryPropertyCollector ) => { const response = new Response ( request ) ; try { logger . verbose ( `From client: ${ request . command }(${ JSON . stringify ( request . arguments ) })` ) ; if ( ! ( request . command in this . _debugAdapter ) ) { reportFailure ( 'The debug adapter doesn\'t recognize this command' ) ; this . sendUnknownCommandResponse ( response , request . command ) ; } else { telemetryPropertyCollector . addTelemetryProperty ( 'requestType' , request . type ) ; response . body = await this . _debugAdapter [ request . command ] ( request . arguments , telemetryPropertyCollector , request . seq ) ; this . sendResponse ( response ) ; } } catch ( e ) { if ( ! this . isEvaluateRequest ( request . command , e ) ) { reportFailure ( e ) ; } this . failedRequest ( request . command , response , e ) ; } } ) ; } private async reportTelemetry ( eventName , action ) { const startProcessingTime = process . hrtime ( ) ; const startTime = Date . now ( ) ; const isSequentialRequest = eventName === 'ClientRequest/initialize' || eventName === 'ClientRequest/launch' || eventName === 'ClientRequest/attach' ; const properties = { } ; const telemetryPropertyCollector = new TelemetryPropertyCollector ( ) ; if ( isSequentialRequest ) { this . events . emitStepStarted ( eventName ) ; } let failed = false ; const sendTelemetry = ( ) => { const timeTakenInMilliseconds = utils . calculateElapsedTime ( startProcessingTime ) ; properties . timeTakenInMilliseconds = timeTakenInMilliseconds . toString ( ) ; if ( isSequentialRequest ) { this . events . emitStepCompleted ( eventName ) ; } else { this . events . emitRequestCompleted ( eventName , startTime , timeTakenInMilliseconds ) ; } Object . assign ( properties , telemetryPropertyCollector . getProperties ( ) ) ; telemetry . reportEvent ( eventName , properties ) ; } ; const reportFailure = e => { failed = true ; properties . successful = 'false' ; properties . exceptionType = 'firstChance' ; utils . fillErrorDetails ( properties , e ) ; sendTelemetry ( ) ; } ; await action ( reportFailure , telemetryPropertyCollector ) ; if ( ! failed ) { properties . successful = 'true' ; sendTelemetry ( ) ; } } private isEvaluateRequest ( requestType , error ) { return ! isMessage ( error ) && ( requestType === 'evaluate' ) ; } private failedRequest ( requestType , response , error ) { if ( isMessage ( error ) ) { this . sendErrorResponse ( response , error as DebugProtocol . Message ) ; return ; } if ( this . isEvaluateRequest ( requestType , error ) ) { response . message = error ? error . message : 'Unknown error' ; response . success = false ; this . sendResponse ( response ) ; return ; } const errUserMsg = isChromeError ( error ) ? error . message + ': ' + error . data : ( error . message || error . stack ) ; const errDiagnosticMsg = isChromeError ( error ) ? errUserMsg : ( error . stack || error . message ) ; logger . error ( `Error processing "${ requestType }": ${ errDiagnosticMsg }` ) ; this . sendErrorResponse ( response , 1104 , '[{_extensionName}] Error processing "{_requestType}": {_stack}' , { _extensionName : this . _extensionName , _requestType : requestType , _stack : errUserMsg } , ErrorDestination . Telemetry ) ; } private sendUnknownCommandResponse ( response , command ) { this . sendErrorResponse ( response , 1014 , `[${ this . _extensionName }] Unrecognized request: ${ command }` , null , ErrorDestination . Telemetry ) ; } public reportTimingsWhileStartingUpIfNeeded ( requestedContentWasDetected , reasonForNotDetected ? ) { if ( ! this . haveTimingsWhileStartingUpBeenReported ) { const report = this . reporter . generateReport ( ) ; const telemetryData = { RequestedContentWasDetected : requestedContentWasDetected . toString ( ) } as { [ key ] : string ; } ; for ( const reportProperty in report ) { telemetryData [ reportProperty ] = JSON . stringify ( report [ reportProperty ] ) ; } if ( ! requestedContentWasDetected && typeof reasonForNotDetected !== 'undefined' ) { telemetryData . RequestedContentWasNotDetectedReason = reasonForNotDetected ; } telemetry . reportEvent ( 'report-start-up-timings' , telemetryData ) ; this . haveTimingsWhileStartingUpBeenReported = true ; } } private configureExecutionTimingsReporting ( ) { this . reporter . subscribeTo ( this . events ) ; this . _debugAdapter . events . once ( ChromeDebugSession . FinishedStartingUpEventName , args => { this . reportTimingsWhileStartingUpIfNeeded ( args ? args . requestedContentWasDetected : true , args && args . reasonForNotDetected ) ; } ) ; setTimeout ( ( ) => this . reportTimingsWhileStartingUpIfNeeded ( false , 'timeout' ) , this . _readyForUserTimeoutInMilliseconds ) ; } public shutdown ( ) { process . removeAllListeners ( 'uncaughtException' ) ; process . removeAllListeners ( 'unhandledRejection' ) ; this . reportTimingsWhileStartingUpIfNeeded ( false , 'shutdown' ) ; super . shutdown ( ) ; } public sendResponse ( response ) { const originalLogVerbose = logger . verbose ; try { logger . verbose = textToLog => { if ( response && response . command === 'source' && response . body && response . body . content ) { const clonedResponse = Object . assign ( { } , response ) ; clonedResponse . body = Object . assign ( { } , response . body ) ; clonedResponse . body . content = '<removed script source for logs>' ; return originalLogVerbose . call ( logger , `To client: ${ JSON . stringify ( clonedResponse ) }` ) ; } else { return originalLogVerbose . call ( logger , textToLog ) ; } } ; super . sendResponse ( response ) ; } finally { logger . verbose = originalLogVerbose ; } } } function logVersionInfo ( ) { logger . log ( `OS: ${ os . platform ( ) } ${ os . arch ( ) }` ) ; logger . log ( `Adapter node: ${ process . version } ${ process . arch }` ) ; const coreVersion = require ( '../../../package.json' ) . version ; logger . log ( 'vscode-chrome-debug-core: ' + coreVersion ) ; telemetry . addCustomGlobalProperty ( { 'Versions.DebugAdapterCore' : coreVersion } ) ; }