import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth" ; import * as request from 'request-promise-native' ; import * as vscode from 'vscode' ; import { Breakpoint , Handles , InitializedEvent , Logger , logger , LoggingDebugSession , OutputEvent , Scope , StackFrame , StoppedEvent , TerminatedEvent , Thread , ThreadEvent , Variable } from 'vscode-debugadapter' ; import { DebugProtocol } from 'vscode-debugprotocol' ; import { localize } from "../localize" ; import { createTemporaryFile } from "../utils/fsUtil" ; import { getBearerToken } from '../utils/requestUtil' ; import { writeToEditor } from '../utils/vscodeUtils' ; import { DebuggerConnection , RequestContract } from './debuggerConnection' ; import { PolicySource } from './policySource' ; import { UiRequest } from './uiRequest' ; import { UiThread } from './uiThread' ; const { Subject } = require ( 'await-notify' ) ; interface ILaunchRequestArguments extends DebugProtocol . LaunchRequestArguments { gatewayAddress : string ; managementAddress : string ; managementAuth : string ; subscriptionId : string ; operationData : string ; fileName : string ; stopOnEntry ? : boolean ; } export class ApimDebugSession extends LoggingDebugSession { private availablePolicies : string [ ] ; private runtime : DebuggerConnection ; private configurationDone = new Subject ( ) ; private requests : UiRequest [ ] = [ ] ; private policySource : PolicySource ; private variablesHandles = new Handles < string > ( ) ; public constructor ( ) { super ( ) ; this . setDebuggerLinesStartAt1 ( false ) ; this . setDebuggerColumnsStartAt1 ( false ) ; this . runtime = new DebuggerConnection ( ) ; this . runtime . on ( 'stopOnEntry' , ( requestId , threadId , operationId , apiId , productId ) => this . onStop ( 'entry' , requestId , threadId , operationId , apiId , productId ) ) ; this . runtime . on ( 'stopOnStep' , ( requestId , threadId , operationId , apiId , productId ) => this . onStop ( 'step' , requestId , threadId , operationId , apiId , productId ) ) ; this . runtime . on ( 'stopOnBreakpoint' , ( requestId , threadId , operationId , apiId , productId ) => this . onStop ( 'breakpoint' , requestId , threadId , operationId , apiId , productId ) ) ; this . runtime . on ( 'stopOnException' , ( requestId , threadId , operationId , apiId , productId , message ) => this . onStop ( 'exception' , requestId , threadId , operationId , apiId , productId , message ) ) ; this . runtime . on ( 'threadExited' , ( requestId , threadId ) => this . onThreadExited ( requestId , threadId ) ) ; this . runtime . on ( 'end' , message => { this . requests = [ ] ; if ( message ) { this . sendEvent ( new OutputEvent ( message , 'stderr' ) ) ; } this . sendEvent ( new TerminatedEvent ( ) ) ; } ) ; } protected initializeRequest ( response , _args ) { response . body = response . body || { } ; response . body . supportsConfigurationDoneRequest = true ; response . body . supportsRestartRequest = false ; response . body . supportsRestartFrame = false ; this . sendResponse ( response ) ; } protected configurationDoneRequest ( response , args ) { super . configurationDoneRequest ( response , args ) ; this . configurationDone . notify ( ) ; } protected async launchRequest ( response , args ) { logger . setup ( Logger . LogLevel . Verbose , false ) ; let masterKey ; if ( args . managementAuth ) { this . policySource = new PolicySource ( args . managementAddress , undefined , args . managementAuth ) ; masterKey = await this . getMasterSubscriptionKey ( args . managementAddress , undefined , args . managementAuth ) ; this . availablePolicies = await this . getAvailablePolicies ( args . managementAddress , undefined , args . managementAuth ) ; } else { const credential = await this . getAccountCredentials ( args . subscriptionId ) ; this . policySource = new PolicySource ( args . managementAddress , credential ) ; masterKey = await this . getMasterSubscriptionKey ( args . managementAddress , credential ) ; this . availablePolicies = await this . getAvailablePolicies ( args . managementAddress , credential ) ; } await this . runtime . attach ( args . gatewayAddress , masterKey , ! ! args . stopOnEntry ) ; this . sendEvent ( new InitializedEvent ( ) ) ; await this . configurationDone . wait ( 1000 ) ; this . sendResponse ( response ) ; this . updateRequests ( await this . runtime . getRequests ( ) , true ) ; await this . createTestOperationFile ( args . operationData , args . fileName ) ; } protected async threadsRequest ( response ) { if ( this . runtime . isConnected ( ) ) { this . updateRequests ( await this . runtime . getRequests ( ) , true ) ; } const threads = [ ] ; for ( const nRequest of this . requests ) { for ( const thread of nRequest . threads ) { threads . push ( new Thread ( thread . uiId , `${ nRequest . id } (${ thread . id })` ) ) ; } } response . body = { threads : threads } ; this . sendResponse ( response ) ; } protected async terminateThreadsRequest ( response , args , _request ? ) { const requests = args . threadIds ! . map ( id => this . findThreadByUiId ( id ) ) ; if ( requests . length ) { await this . runtime . terminateRequests ( requests . map ( r => r ! [ 0 ] . id ) . filter ( ( value , index , self ) => self . indexOf ( value ) === index ) ) ; } this . sendResponse ( response ) ; } protected async stackTraceRequest ( response , args ) { let stack = [ ] ; if ( this . runtime . isConnected ( ) ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest ) { const requestStack = await this . runtime . getStackTrace ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; stack = await nRequest [ 1 ] . getStackFrames ( requestStack ) ; for ( const item of < DebugProtocol . StackFrame [ ] > stack ) { if ( item . line ) { item . line = this . convertDebuggerLineToClient ( item . line ) ; } if ( item . column ) { item . column = this . convertDebuggerColumnToClient ( item . column ) ; } if ( item . endLine ) { item . endLine = this . convertDebuggerLineToClient ( item . endLine ) ; } if ( item . endColumn ) { item . endColumn = this . convertDebuggerColumnToClient ( item . endColumn ) + 1 ; } } } } response . body = { stackFrames : stack , totalFrames : stack . length } ; this . sendResponse ( response ) ; } protected async sourceRequest ( response , args ) { const policy = this . policySource . getPolicyBySourceReference ( args . sourceReference ) ; if ( policy !== null && policy . xml !== null ) { response . body = { content : policy && policy . xml , mimeType : 'application/vnd.ms-azure-apim.policy.raw+xml' } ; this . sendResponse ( response ) ; } } protected async continueRequest ( response , args ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest && this . runtime . isConnected ( ) ) { await this . runtime . continue ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; } response . body = { allThreadsContinued : false } ; this . sendResponse ( response ) ; } protected async nextRequest ( response , args ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest && this . runtime . isConnected ( ) ) { await this . runtime . stepOver ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; } this . sendResponse ( response ) ; } protected async stepInRequest ( response , args ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest && this . runtime . isConnected ( ) ) { await this . runtime . stepIn ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; } this . sendResponse ( response ) ; } protected async stepOutRequest ( response , args ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest && this . runtime . isConnected ( ) ) { await this . runtime . stepOut ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; } this . sendResponse ( response ) ; } protected async pauseRequest ( response , args ) { const nRequest = this . findThreadByUiId ( args . threadId ) ; if ( nRequest && this . runtime . isConnected ( ) ) { await this . runtime . pause ( nRequest [ 0 ] . id , nRequest [ 1 ] . id ) ; } this . sendResponse ( response ) ; } protected scopesRequest ( response , args ) { let scopes = [ ] ; const nRequest = this . findThreadByStackFrameId ( args . frameId ) ; if ( nRequest ) { scopes = [ new Scope ( "Request" , this . variablesHandles . create ( `${ nRequest [ 0 ] . id }|${ nRequest [ 1 ] . id }` ) , true ) ] ; } response . body = { scopes : scopes || [ ] } ; this . sendResponse ( response ) ; } protected async variablesRequest ( response , args ) { let variables = [ ] ; if ( this . runtime . isConnected ( ) ) { const variableScope = this . variablesHandles . get ( args . variablesReference ) ; const scopeParts = variableScope . split ( '|' ) ; if ( scopeParts . length >= 2 ) { const vars = await this . runtime . getVariables ( scopeParts [ 0 ] , + scopeParts [ 1 ] , scopeParts . slice ( 2 ) . join ( '.' ) ) ; variables = vars . map ( v => { const variable = new Variable ( v . name , v . value || "" , v . nestedCount ? this . variablesHandles . create ( `${ variableScope }|${ v . name }` ) : 0 ) ; ( < DebugProtocol . Variable > variable ) . type = v . type ; ( < DebugProtocol . Variable > variable ) . namedVariables = v . nestedCount ; ( < DebugProtocol . Variable > variable ) . presentationHint = { kind : 'property' , visibility : 'public' } ; return variable ; } ) ; } } response . body = { variables : variables || [ ] } ; this . sendResponse ( response ) ; } protected async setBreakPointsRequest ( response , args ) { const breakpoints = await this . setBreakpoints ( args ) ; const nBreakpoints = ( breakpoints . length !== 0 ) ? breakpoints : ( args . breakpoints ) ? args . breakpoints . map ( _b => new Breakpoint ( false ) ) : [ ] ; response . body = { breakpoints : nBreakpoints } ; this . sendResponse ( response ) ; } private async createTestOperationFile ( operationData , fileName ) { const localFilePath = await createTemporaryFile ( fileName ) ; const document = await vscode . workspace . openTextDocument ( localFilePath ) ; const textEditor = await vscode . window . showTextDocument ( document ) ; await writeToEditor ( textEditor , operationData ) ; await textEditor . document . save ( ) ; } private async setBreakpoints ( args ) { let breakpoints = [ ] ; const breakpointsToSet = [ ] ; let policy = this . policySource . getPolicyBySourceReference ( args . source . sourceReference ) ; if ( ! policy && args . source . name ) { policy = this . policySource . getPolicy ( args . source . name ) || await this . policySource . fetchPolicy ( args . source . name ) ; } if ( args . breakpoints && args . breakpoints . length ) { if ( policy && policy !== null ) { breakpoints = args . breakpoints . map ( b => { let position = { line : - 1 , column : - 1 , endLine : - 1 , endColumn : - 1 } ; let path = null ; const breakpointLine = this . convertClientLineToDebugger ( b . line ) ; const breakpointColumn = b . column && this . convertClientColumnToDebugger ( b . column ) ; for ( const key in policy ! . map ) { const mapEntry = policy ! . map [ key ] ; if ( mapEntry . line === breakpointLine && ( ! breakpointColumn || breakpointColumn >= mapEntry . column && breakpointColumn <= mapEntry . endColumn ) && ( position . line === - 1 || mapEntry . column < position . column ) ) { path = key ; position = mapEntry ; } } if ( position . line === - 1 ) { return new Breakpoint ( false ) ; } if ( path === null ) { throw new Error ( "Path is null" ) ; } let policyName = path . substring ( path . lastIndexOf ( '/' ) + 1 ) ; if ( policyName . indexOf ( '[' ) !== - 1 ) { policyName = policyName . substring ( 0 , policyName . lastIndexOf ( '[' ) ) ; } if ( this . availablePolicies . indexOf ( policyName ) === - 1 ) { return new Breakpoint ( false ) ; } breakpointsToSet . push ( { path : path . substr ( path . indexOf ( '/' ) + 1 ) , scopeId : policy ! . scopeId } ) ; const breakpoint = new Breakpoint ( true , this . convertDebuggerLineToClient ( position . line ) , this . convertDebuggerColumnToClient ( position . column ) , policy ! . source ) ; ( < DebugProtocol . Breakpoint > breakpoint ) . endLine = this . convertDebuggerLineToClient ( position . endLine ) ; ( < DebugProtocol . Breakpoint > breakpoint ) . endColumn = this . convertDebuggerColumnToClient ( position . endColumn ) ; return breakpoint ; } ) ; } } await this . runtime . setBreakpoints ( breakpointsToSet , policy ! . scopeId ) ; return breakpoints ; } private onThreadExited ( requestId , threadId ) { const nRequest = this . requests . find ( r => r . id === requestId ) ; const thread = nRequest && nRequest . findThreadById ( threadId ) ; if ( thread ) { this . sendEvent ( new ThreadEvent ( 'exited' , thread . uiId ) ) ; } } private onStop ( event , requestId , threadId , operationId , apiId , productId , exceptionText ? ) { this . updateRequests ( [ { id : requestId , threads : [ threadId ] , operationId : operationId , apiId : apiId , productId : productId } ] , false ) ; const nRequest = this . requests . find ( r => r . id === requestId ) ; const thread = nRequest && nRequest . findThreadById ( threadId ) ; if ( thread ) { this . sendEvent ( new StoppedEvent ( event , thread . uiId , exceptionText ) ) ; } } private async getAccountCredentials ( subscriptionId ) { const azureAccountExtension = vscode . extensions . getExtension ( 'ms-vscode.azure-account' ) ; const azureAccount = azureAccountExtension ! . exports ; await azureAccount . waitForFilters ( ) ; if ( azureAccount . status !== 'LoggedIn' ) { throw new Error ( "ERROR!" ) ; } const creds = azureAccount . filters . filter ( filter => filter . subscription . subscriptionId === subscriptionId ) . map ( filter => filter . session . credentials ) ; return creds [ 0 ] ; } private async getMasterSubscriptionKey ( managementAddress , credential ? , managementAuth ? ) { const resourceUrl = `${ managementAddress }/subscriptions/master?api-version=2019-01-01` ; const authToken = managementAuth ? managementAuth : await getBearerToken ( resourceUrl , "GET" , credential ! ) ; const subscription = await request . get ( resourceUrl , { headers : { Authorization : authToken } , strictSSL : false , json : true } ) . on ( 'error' , _e => { this . sendEvent ( new TerminatedEvent ( ) ) ; } ) . on ( 'response' , e => { if ( e . statusCode !== 200 ) { this . sendEvent ( new OutputEvent ( localize ( "" , `Error fetching master subscription: ${ e . statusCode } ${ e . statusMessage }` , 'stderr' ) ) ) ; this . sendEvent ( new TerminatedEvent ( ) ) ; } } ) ; return subscription . properties . primaryKey ; } private async getAvailablePolicies ( managementAddress , credential ? , managementAuth ? ) { const resourceUrl = `${ managementAddress }/policysnippets?api-version=2019-01-01` ; const authToken = managementAuth ? managementAuth : await getBearerToken ( resourceUrl , "GET" , credential ! ) ; const snippets = await request . get ( resourceUrl , { headers : { Authorization : authToken } , strictSSL : false , json : true } ) . on ( 'error' , _e => { this . sendEvent ( new TerminatedEvent ( ) ) ; } ) . on ( 'response' , e => { if ( e . statusCode !== 200 ) { this . sendEvent ( new OutputEvent ( localize ( "" , `Error fetching policy definitions: ${ e . statusCode } ${ e . statusMessage }` , 'stderr' ) ) ) ; this . sendEvent ( new TerminatedEvent ( ) ) ; } } ) ; const allPolicies = [ ] ; let index = 0 ; for ( const snippet of snippets ) { if ( snippet . content !== undefined && snippet . content !== "" ) { const idx = /[\s>/]/ . exec ( snippet . content ) ! . index ; allPolicies [ index ] = snippet . content . substring ( 1 , idx ) ; index += 1 ; } } return allPolicies ; } private findThreadByUiId ( id ) { for ( const uiRequest of this . requests ) { const uiThread = uiRequest . findThreadByUiId ( id ) ; if ( uiThread ) { return [ uiRequest , uiThread ] ; } } return null ; } private findThreadByStackFrameId ( id ) { for ( const uiRequest of this . requests ) { const uiThread = uiRequest . findThreadByStackFrameId ( id ) ; if ( uiThread ) { return [ uiRequest , uiThread ] ; } } return null ; } private updateRequests ( gatewayRequests , clean ) { if ( clean ) { let requestIndex = 0 ; while ( requestIndex < this . requests . length ) { const uiRequest = this . requests [ requestIndex ] ; const gatewayRequest = gatewayRequests . find ( r => r . id === uiRequest . id ) ; if ( ! gatewayRequest ) { for ( const thread of uiRequest . threads ) { this . sendEvent ( new ThreadEvent ( 'exited' , thread . uiId ) ) ; } this . requests . splice ( requestIndex , 1 ) ; continue ; } let threadIndex = 0 ; while ( threadIndex < uiRequest . threads . length ) { const uiThread = uiRequest . threads [ threadIndex ] ; if ( gatewayRequest . threads . indexOf ( uiThread . id ) < 0 ) { this . sendEvent ( new ThreadEvent ( 'exited' , uiThread . uiId ) ) ; uiRequest . threads . splice ( threadIndex , 1 ) ; continue ; } threadIndex ++ ; } if ( ! uiRequest . threads . length ) { this . requests . splice ( requestIndex , 1 ) ; continue ; } requestIndex ++ ; } } for ( const gatewayRequest of gatewayRequests ) { let uiRequest = this . requests . find ( r => r . id === gatewayRequest . id ) ; if ( ! uiRequest ) { this . requests . push ( uiRequest = new UiRequest ( gatewayRequest . id , gatewayRequest . operationId , gatewayRequest . apiId , gatewayRequest . productId ) ) ; } for ( const gatewayThread of gatewayRequest . threads ) { let uiThread = uiRequest . threads . find ( t => t . id === gatewayThread ) ; if ( ! uiThread ) { uiThread = uiRequest . addNewThread ( gatewayThread , this . policySource ) ; this . sendEvent ( new ThreadEvent ( 'started' , uiThread . uiId ) ) ; } } } } } interface IApimSubscription { properties : { primaryKey : string ; secondaryKey : string ; } ; } interface IPolicySnippet { content : string ; }