/*******************************************************************************
Copyright 2018 Paulo Silva

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ******************************************************************************/ import { DebugSession , LoggingDebugSession , InitializedEvent , TerminatedEvent , StoppedEvent , Thread , StackFrame , Scope , Breakpoint , Variable } from 'vscode-debugadapter' ; import { OctaveLogger } from './OctaveLogger' ; import { DebugProtocol } from 'vscode-debugprotocol' ; import * as Constants from './Constants' ; import { Runtime } from './Runtime' ; import { CommandInterface } from './Commands' ; import { Breakpoints } from './Breakpoints' ; import { Expression } from './Expression' ; import { StackFramesManager } from './StackFramesManager' ; import { Variables } from './Variables/Variables' ; import { Variable as OctaveVariable } from './Variables/Variable' ; import { UnknownType } from './Variables/UnknownType' ; import { Scalar } from './Variables/Scalar' ; import { Bool } from './Variables/Bool' ; import { String } from './Variables/String' ; import { Matrix } from './Variables/Matrix' ; import { SparseMatrix } from './Variables/SparseMatrix' ; import { ComplexDiagonalMatrix } from './Variables/ComplexDiagonalMatrix' ; import { LazyIndex } from './Variables/LazyIndex' ; import { Range } from './Variables/Range' ; import { Struct } from './Variables/Struct' ; import { ScalarStruct } from './Variables/ScalarStruct' ; import { Function } from './Variables/Function' ; import { Cell } from './Variables/Cell' ; import { Scope as OctaveScope } from './Variables/Scope' ; import { isMatlabFile , validDirectory } from './Utils/fsutils' ; import { dirname } from 'path' ; import { CommandList } from './CommandList' ; interface LaunchRequestArguments extends DebugProtocol . LaunchRequestArguments { /** An absolute path to the "program" to debug. */ program : string ; /** Path to the octave-cli. */ octave : string ; /** Absolute path to the project source folder. Defaults to workspace folder. */ sourceFolder ? : string ; /** Absolute path to the desired working directory. Defaults to program location. */ workingDirectory ? : string ; /** Enable verbose logging of the Debug Adapter Protocol. */ verbose ? : boolean ; /** Output verbose logging to file. */ logFilename ? : string ; /** Maximum number of chunks of elements to prefetch. */ prefetchCount ? : number ; /** Enable ans evaluation. */ evaluateAns ? : boolean ; /** Terminates Octave process when debug runs beyond the end of the program. */ autoTerminate ? : boolean ; /** Enable fieldnames containing an almost arbitrary format. */ splitFieldnamesOctaveStyle ? : boolean ; /** Command line arguments to be passed to the octave process. */ octaveArguments ? : string [ ] ; /** Environment to be set when octave runs. Json key:value pair object. */ octaveEnvironment ? : any ; /** Run the octave process in a shell. Allows things like 'octave': 'export FOO=bar; octave-cli'. */ shell ? : boolean ; } class OctaveDebugSession extends LoggingDebugSession { private static readonly THREAD_ID = 1 ; private _stepCount = 0 ; private _runtime : Runtime ; private _stackManager : StackFramesManager ; private _breakpointsCallbacks = new Array < ( ci : CommandInterface ) => void > ( ) ; private _stepping : boolean ; private _runCallback : ( ) => void ; private _configurationDone = false ; public constructor ( ) { super ( ) ; this . _stackManager = new StackFramesManager ( ) ; this . setDebuggerLinesStartAt1 ( true ) ; this . setDebuggerColumnsStartAt1 ( true ) ; this . setupVariables ( ) ; } private setupVariables ( ) { Variables . register ( new Range ( ) ) ; Variables . register ( new Scalar ( ) ) ; Variables . register ( new SparseMatrix ( ) ) ; Variables . register ( new ComplexDiagonalMatrix ( ) ) ; Variables . register ( new Matrix ( ) ) ; Variables . register ( new LazyIndex ( ) ) ; Variables . register ( new Struct ( ) ) ; Variables . register ( new ScalarStruct ( ) ) ; Variables . register ( new String ( ) ) ; Variables . register ( new Bool ( ) ) ; Variables . register ( new Function ( ) ) ; Variables . register ( new Cell ( ) ) ; Variables . registerFallback ( new UnknownType ( ) ) ; } private sendTerminatedEvent ( ) { this . sendEvent ( new TerminatedEvent ( ) ) ; } public sendBreakEvent ( ) { this . sendEvent ( new StoppedEvent ( 'breakpoint' , OctaveDebugSession . THREAD_ID ) ) ; this . _stepping = false ; } private setupRuntime ( octave , octaveArgs , environment , sourcefolders , workingDirectory , autoTerminate , shell ) { if ( this . _runtime ) { return ; } OctaveLogger . logToConsole = this . _isServer ; this . _runtime = new Runtime ( octave , octaveArgs , environment , sourcefolders , workingDirectory , autoTerminate , shell ) ; if ( ! this . runtimeConnected ( ) ) { OctaveLogger . warn ( `Could not connect to '${ octave }'! Check path.` ) ; return ; } this . _runtime . on ( Constants . eEXIT , ( ) => this . sendTerminatedEvent ( ) ) ; this . _runtime . on ( Constants . eEND , ( ) => this . sendTerminatedEvent ( ) ) ; this . _runtime . on ( Constants . eERROR , ( ) => this . sendTerminatedEvent ( ) ) ; this . _runtime . on ( Constants . eBREAK , ( ) => this . sendBreakEvent ( ) ) ; } public runtimeConnected ( ) { return this . _runtime . connected ( ) ; } protected initializeRequest ( response , args ) { this . sendEvent ( new InitializedEvent ( ) ) ; response . body = response . body || { } ; response . body . supportsConfigurationDoneRequest = true ; response . body . supportsEvaluateForHovers = true ; response . body . supportsConditionalBreakpoints = true ; response . body . supportsHitConditionalBreakpoints = true ; response . body . supportsSetVariable = true ; response . body . supportsStepBack = false ; response . body . supportTerminateDebuggee = true ; this . sendResponse ( response ) ; } protected disconnectRequest ( response , args ) { this . _runtime . disconnect ( ) ; this . sendResponse ( response ) ; } protected configurationDoneRequest ( response , args ) { super . configurationDoneRequest ( response , args ) ; this . _configurationDone = true ; if ( this . _runCallback ) { this . _runCallback ( ) ; } } private static getSourceFolders ( args ) { let sourceFolders = this . val ( args . sourceFolder , '' ) ; let programDirectory = dirname ( args . program ) ; if ( validDirectory ( programDirectory ) ) { if ( sourceFolders !== '' ) { sourceFolders = programDirectory + ':' + sourceFolders ; } else { sourceFolders = programDirectory ; } } return sourceFolders ; } private static getWorkingDirectory ( args ) { if ( args . workingDirectory !== undefined && validDirectory ( args . workingDirectory ) ) { return args . workingDirectory ; } const programDirectory = dirname ( args . program ) ; if ( validDirectory ( programDirectory ) ) { return programDirectory ; } if ( args . sourceFolder !== undefined && validDirectory ( args . sourceFolder ) ) { return args . sourceFolder ; } return '' ; } private static val ( value , _default ) { if ( value !== undefined && value !== null ) { return value ; } return _default ; } protected async launchRequest ( response , args ) { OctaveLogger . setup ( args . verbose , args . logFilename ) ; Variables . evaluateAns = OctaveDebugSession . val ( args . evaluateAns , false ) ; if ( args . splitFieldnamesOctaveStyle !== undefined ) { ScalarStruct . setSplitStyle ( args . splitFieldnamesOctaveStyle ) ; } if ( args . prefetchCount !== undefined ) { Variables . setChunkPrefetch ( args . prefetchCount ) ; } this . setupRuntime ( OctaveDebugSession . val ( args . octave , Constants . DEFAULT_EXECUTABLE ) , OctaveDebugSession . val ( args . octaveArguments , [ ] ) , OctaveDebugSession . val ( args . octaveEnvironment , { } ) , OctaveDebugSession . getSourceFolders ( args ) , OctaveDebugSession . getWorkingDirectory ( args ) , OctaveDebugSession . val ( args . autoTerminate , true ) , OctaveDebugSession . val ( args . shell , true ) ) ; if ( this . runtimeConnected ( ) ) { this . runSetBreakpoints ( ) ; if ( ! this . _configurationDone ) { this . _runCallback = ( ) => { this . _runtime . start ( args . program ) ; } ; } else { this . _runtime . start ( args . program ) ; } } this . sendResponse ( response ) ; } private runSetBreakpoints ( ) { if ( this . _runtime && this . _breakpointsCallbacks . length !== 0 ) { const cl = new CommandList ( this . _stepCount ) ; this . _breakpointsCallbacks . forEach ( callback => { callback ( cl ) ; } ) ; this . _breakpointsCallbacks . length = 0 ; this . executeCommandList ( cl , ( ) => { } ) ; } } private handleInvalidBreakpoints ( response , args ) { if ( args . breakpoints !== undefined ) { const breakpoints = args . breakpoints . map ( bp => < Breakpoint > { verified : false , source : args . source , line : bp . line } ) ; response . body = { breakpoints : breakpoints } ; this . sendResponse ( response ) ; } } protected setBreakPointsRequest ( response , args ) { if ( ! isMatlabFile ( args . source ) ) { return this . handleInvalidBreakpoints ( response , args ) ; } const callback = ( ci ) => { const vscBreakpoints = args . breakpoints ; if ( vscBreakpoints !== undefined && args . source . path !== undefined ) { const path = < string > args . source . path ; Breakpoints . replaceBreakpoints ( vscBreakpoints , path , ci , ( breakpoints ) => { response . body = { breakpoints : breakpoints } ; this . sendResponse ( response ) ; } ) ; } else { this . sendResponse ( response ) ; } } ; if ( this . _runtime ) { const cl = new CommandList ( this . _stepCount ) ; callback ( cl ) ; this . executeCommandList ( cl , ( ) => { } ) ; } else { this . _breakpointsCallbacks . push ( callback ) ; } } protected threadsRequest ( response ) { response . body = { threads : [ new Thread ( OctaveDebugSession . THREAD_ID , `thread ${ OctaveDebugSession . THREAD_ID }` ) ] } ; this . sendResponse ( response ) ; } protected stackTraceRequest ( response , args ) { const startFrame = typeof args . startFrame === 'number' ? args . startFrame : 0 ; const maxLevels = typeof args . levels === 'number' ? args . levels : 1000 ; const endFrame = startFrame + maxLevels ; this . clear ( ) ; const callback = ( stackFrames ) => { if ( stackFrames . length == 0 && ! this . _runtime . autoTerminate ( ) ) { stackFrames . push ( new StackFrame ( 1 , "at top level" ) ) ; } response . body = { stackFrames : stackFrames , totalFrames : stackFrames . length } ; this . sendResponse ( response ) ; } ; this . _stackManager . get ( startFrame , endFrame , this . _runtime , callback ) ; } protected scopesRequest ( response , args ) { const callback = ( ) => { const localScope = new OctaveScope ( '' ) ; const globalScope = new OctaveScope ( 'global' ) ; response . body = { scopes : [ new Scope ( 'local' , localScope . reference ( ) , false ) , new Scope ( globalScope . name ( ) , globalScope . reference ( ) , false ) ] } ; this . sendResponse ( response ) ; } ; this . _stackManager . selectStackFrame ( args . frameId , this . _runtime , callback ) ; } private static readonly EMPTY_VARS = new Array < OctaveVariable > ( ) ; protected variablesRequest ( response , args ) { const callback = ( variables ) => { response . body = { variables : variables . map ( v => < Variable > { name : v . name ( ) , type : v . extendedTypename ( ) , value : v . value ( ) , variablesReference : v . reference ( ) , namedVariables : v . namedVariables ( ) , indexedVariables : v . indexedVariables ( ) } ) } ; this . sendResponse ( response ) ; } ; const count = args . count || 0 ; const start = args . start || 0 ; const cl = new CommandList ( this . _stepCount ) ; Variables . listByReference ( args . variablesReference , cl , count , start , callback ) ; this . executeCommandList ( cl , ( ) => { callback ( OctaveDebugSession . EMPTY_VARS ) ; } ) ; } protected setVariableRequest ( response , variable ) { Variables . setVariable ( variable . name , variable . value , this . _runtime , ( newValue ) => { response . body = { value : newValue } ; this . sendResponse ( response ) ; } ) ; } protected evaluateRequest ( response , args ) { const sendResponse = ( val ) => { response . body = { result : val , variablesReference : 0 } ; this . sendResponse ( response ) ; } ; Expression . evaluate ( args . expression , this . _runtime , args . context , sendResponse ) ; } protected stepWith ( cmd ) { this . _stepping = this . _runtime . autoTerminate ( ) ; ++ this . _stepCount ; this . _runtime . evaluateAsLine ( cmd , ( output ) => { OctaveLogger . log ( output ) ; if ( this . _stepping ) { this . _stepping = false ; this . _runtime . evaluateAsLine ( 'dbwhere' , ( output ) => { const WHERE_REGEX = /at top level/ ; if ( output . match ( WHERE_REGEX ) ) { this . sendTerminatedEvent ( ) ; } } ) ; } } ) ; } protected continueRequest ( response , args ) { this . _runtime . execute ( 'dbcont' ) ; this . sendResponse ( response ) ; } protected nextRequest ( response , args ) { this . stepWith ( 'dbstep' ) ; this . sendResponse ( response ) ; } protected stepInRequest ( response , args ) { this . stepWith ( 'dbstep in' ) ; this . sendResponse ( response ) ; } protected stepOutRequest ( response , args ) { this . stepWith ( 'dbstep out' ) ; this . sendResponse ( response ) ; } protected pauseRequest ( response , args ) { OctaveLogger . warn ( 'Pausing...' ) ; this . _runtime . pause ( ) ; this . sendResponse ( response ) ; } private clear ( ) { Variables . clearReferences ( ) ; this . _stackManager . clear ( ) ; } private executeCommandList ( cl , canceled ) { const self = this ; const execute = ( cl ) => { if ( ! cl . empty ( ) ) { if ( cl . id ( ) === self . _stepCount ) { cl . executeCommandList ( self . _runtime , execute ) ; } else { canceled ( ) ; } } } ; execute ( cl ) ; } } DebugSession . run ( OctaveDebugSession ) ;