file import * as child_process from "child_process" ; import { SpawnOptionsWithoutStdio } from "child_process" ; import * as http from "http" ; import * as net from "net" ; import * as os from "os" ; import { Writable } from "stream" ; import { OutputEvent } from "vscode-debugadapter" ; import { DebugProtocol } from "vscode-debugprotocol" ; const TWO_CRLF = "\r\n\r\n" ; const CONTENT_LENGTH_PATTERN = new RegExp ( "Content-Length: (\\d+)" ) ; const DEFAULT_HHVM_DEBUGGER_PORT = 8999 ; const DEFAULT_HHVM_PATH = "hhvm" ; type DebuggerWriteCallback = ( data : string ) => void ; interface HhvmAttachRequestArguments extends DebugProtocol . AttachRequestArguments { host ? : string ; port ? : string ; socket ? : string ; remoteSiteRoot ? : string ; localWorkspaceRoot ? : string ; sandboxUser ? : string ; launchUrl ? : string ; } interface HhvmLaunchRequestArguments extends DebugProtocol . LaunchRequestArguments { hhvmPath ? : string ; hhvmArgs ? : string [ ] ; script ? : string ; cwd ? : string ; deferLaunch ? : boolean ; sandboxUser ? : string ; localWorkspaceRoot ? : string ; remoteEnabled ? : boolean ; remoteType ? : string ; remoteWorkspacePath ? : string ; dockerContainerName ? : string ; } class HHVMDebuggerWrapper { private sequenceNumber : number ; private currentOutputData : string ; private currentInputData : string ; private currentContentLength : number ; private bufferedRequests : DebugProtocol . Request [ ] ; private debugging : boolean ; private debuggerWriteCallback ? : DebuggerWriteCallback ; private nonLoaderBreakSeen : boolean ; private initializeArgs : DebugProtocol . InitializeRequestArguments ; private asyncBreakPending : boolean ; private remoteSiteRoot : string | undefined ; private remoteSiteRootPattern : RegExp | undefined ; private localWorkspaceRoot : string | undefined ; private localWorkspaceRootPattern : RegExp | undefined ; constructor ( ) { this . sequenceNumber = 0 ; this . currentContentLength = 0 ; this . currentOutputData = "" ; this . currentInputData = "" ; this . bufferedRequests = [ ] ; this . debugging = false ; this . nonLoaderBreakSeen = false ; this . initializeArgs = { adapterID : "hhvm" } ; this . asyncBreakPending = false ; } public debug ( ) { process . stdin . on ( "data" , ( chunk ) => { this . processClientMessage ( chunk ) ; } ) ; process . on ( "disconnect" , ( ) => { process . exit ( ) ; } ) ; } private attachTarget ( attachMessage , retries = 0 ) { const args = attachMessage . arguments ; const attachPort = args . port ? parseInt ( args . port , 10 ) : DEFAULT_HHVM_DEBUGGER_PORT ; this . remoteSiteRoot = args . remoteSiteRoot ; this . remoteSiteRootPattern = args . remoteSiteRoot ? new RegExp ( this . escapeRegExp ( args . remoteSiteRoot ) , "g" ) : undefined ; this . localWorkspaceRoot = args . localWorkspaceRoot ; this . localWorkspaceRootPattern = args . localWorkspaceRoot ? new RegExp ( this . escapeRegExp ( args . localWorkspaceRoot ) , "g" ) : undefined ; if ( ! args . socket && Number . isNaN ( attachPort ) ) { throw new Error ( "Invalid HHVM debug port or socket path." ) ; } if ( ! args . sandboxUser || args . sandboxUser . trim ( ) === "" ) { args . sandboxUser = os . userInfo ( ) . username ; } const socketArgs = args . socket ? { path : args . socket } : { port : attachPort } ; const socket = net . createConnection ( socketArgs ) ; socket . on ( "data" , ( chunk ) => { this . processDebuggerMessage ( chunk ) ; } ) ; socket . on ( "close" , ( ) => { this . writeOutputWithHeader ( { seq : ++ this . sequenceNumber , type : "event" , event : "hhvmConnectionDied" , } ) ; process . stderr . write ( "The connection to the debug target has been closed." ) ; process . exit ( 0 ) ; } ) ; const callback = ( data ) => { let mappedData = data ; if ( this . localWorkspaceRootPattern && this . remoteSiteRoot ) { mappedData = mappedData . replace ( this . localWorkspaceRootPattern , this . remoteSiteRoot ) ; } socket . write ( `${ mappedData }\0` , "utf8" ) ; } ; callback ( JSON . stringify ( attachMessage ) ) ; this . debuggerWriteCallback = callback ; this . forwardBufferedMessages ( ) ; this . debugging = true ; const attachResponse = { request_seq : attachMessage . seq , success : true , command : attachMessage . command , } ; this . writeResponseMessage ( attachResponse ) ; socket . on ( "error" , ( error ) => { if ( retries >= 5 ) { process . stderr . write ( `Error communicating with debugger target: ${ error . toString ( ) }\n` ) ; process . exit ( ( < any > error ) . code ) ; } else { setTimeout ( ( ) => this . attachTarget ( attachMessage , retries + 1 ) , 2000 ) ; } } ) ; if ( args . launchUrl ) { http . get ( args . launchUrl , ( res ) => { res . on ( "data" , ( data ) => { this . writeOutputEvent ( "stdout" , data . toString ( ) ) ; } ) ; res . on ( "end" , ( ) => { process . exit ( ) ; } ) ; } ) . on ( "error" , ( e ) => { this . writeOutputEvent ( "stdout" , e . message ) ; process . exit ( ) ; } ) ; } } private launchTarget ( launchMessage ) { const args = launchMessage . arguments ; if ( args . deferLaunch ) { this . launchTargetInTerminal ( launchMessage ) ; return ; } let hhvmPath = args . hhvmPath ; if ( ! hhvmPath || hhvmPath === "" ) { hhvmPath = DEFAULT_HHVM_PATH ; } if ( ! args . sandboxUser || args . sandboxUser . trim ( ) === "" ) { args . sandboxUser = os . userInfo ( ) . username ; } const hhvmArgs = args . hhvmArgs || [ ] ; let scriptArgs = args . script ? args . script . split ( " " ) : [ ] ; const dockerArgs = [ ] ; if ( args . remoteEnabled && args . remoteType === "docker" && args . dockerContainerName ) { hhvmPath = "docker" ; dockerArgs . push ( "exec" , "-i" , args . dockerContainerName , "hhvm" ) ; scriptArgs = scriptArgs . map ( ( value ) => value . replace ( args . localWorkspaceRoot || "" , args . remoteWorkspacePath || "" ) ) ; this . remoteSiteRoot = args . remoteWorkspacePath ; this . remoteSiteRootPattern = args . remoteWorkspacePath ? new RegExp ( this . escapeRegExp ( args . remoteWorkspacePath ) , "g" ) : undefined ; this . localWorkspaceRoot = args . localWorkspaceRoot ; this . localWorkspaceRootPattern = args . localWorkspaceRoot ? new RegExp ( this . escapeRegExp ( args . localWorkspaceRoot ) , "g" ) : undefined ; } const allArgs = [ ... dockerArgs , "--mode" , "vsdebug" , ... hhvmArgs , ... scriptArgs , ] ; const options = { cwd : args . cwd || process . cwd ( ) , stdio : Boolean ( args . noDebug ) ? [ "pipe" , "pipe" , "pipe" ] : [ "pipe" , "pipe" , "pipe" , "pipe" ] , detached : false , env : process . env , } ; const targetProcess = child_process . spawn ( hhvmPath , allArgs , options ) ; targetProcess . on ( "exit" , ( code ) => process . exit ( code || undefined ) ) ; targetProcess . on ( "error" , ( error ) => process . stderr . write ( `${ error . toString ( ) }\n` ) ) ; targetProcess . stdout . on ( "data" , ( chunk ) => { const block = chunk . toString ( ) ; this . writeOutputEvent ( "stdout" , block ) ; } ) ; targetProcess . stdout . on ( "error" , ( ) => { } ) ; targetProcess . stderr . on ( "data" , ( chunk ) => { const block = chunk . toString ( ) ; this . writeOutputEvent ( "stderr" , block ) ; } ) ; targetProcess . stderr . on ( "error" , ( ) => { } ) ; if ( targetProcess . stdio [ 3 ] ) { targetProcess . stdio [ 3 ] . on ( "data" , ( chunk ) => { this . processDebuggerMessage ( chunk ) ; } ) ; targetProcess . stdio [ 3 ] . on ( "error" , ( ) => { } ) ; } const callback = ( data ) => { let mappedData = data ; if ( this . localWorkspaceRootPattern && this . remoteSiteRoot ) { mappedData = mappedData . replace ( this . localWorkspaceRootPattern , this . remoteSiteRoot ) ; } ( < Writable > targetProcess . stdio [ 3 ] ) . write ( `${ mappedData }\0` , "utf8" ) ; } ; callback ( JSON . stringify ( launchMessage ) ) ; this . debuggerWriteCallback = callback ; this . forwardBufferedMessages ( ) ; this . debugging = true ; } private launchTargetInTerminal ( requestMessage ) { const startupArgs = requestMessage . arguments ; let hhvmPath = startupArgs . hhvmPath ; if ( ! hhvmPath || hhvmPath === "" ) { hhvmPath = DEFAULT_HHVM_PATH ; } const terminalArgs = [ hhvmPath ] ; if ( startupArgs . hhvmArgs ) { for ( const arg of startupArgs . hhvmArgs ) { terminalArgs . push ( String ( arg ) ) ; } } const runInTerminalArgs = { kind : "integrated" , cwd : __dirname , args : terminalArgs , } ; this . writeOutputWithHeader ( { seq : ++ this . sequenceNumber , type : "request" , command : "runInTerminal" , arguments : runInTerminalArgs , } ) ; const attachConfig = { sandboxUser : startupArgs . sandboxUser , } ; this . attachTarget ( { ... requestMessage , arguments : attachConfig } ) ; } private forwardBufferedMessages ( ) { if ( this . debuggerWriteCallback ) { const callback = this . debuggerWriteCallback ; for ( const requestMsg of this . bufferedRequests ) { callback ( JSON . stringify ( requestMsg ) ) ; } } } private processClientMessage ( chunk ) { this . currentInputData += chunk . toString ( ) ; while ( true ) { if ( this . currentContentLength === 0 ) { this . readContentHeader ( ) ; } const length = this . currentContentLength ; if ( length === 0 || this . currentInputData . length < length ) { break ; } const message = this . currentInputData . substr ( 0 , length ) ; const requestMsg = JSON . parse ( message ) ; if ( ! this . handleWrapperRequest ( requestMsg ) ) { const callback = this . debuggerWriteCallback ; if ( callback ) { callback ( this . translateNuclideRequest ( requestMsg ) ) ; } } this . currentContentLength = 0 ; this . currentInputData = this . currentInputData . substr ( length ) ; } } private translateNuclideRequest ( requestMsg ) { if ( requestMsg . command && requestMsg . command . startsWith ( "nuclide_" ) ) { requestMsg . command = requestMsg . command . replace ( "nuclide_" , "fb_" ) ; return JSON . stringify ( requestMsg ) ; } return JSON . stringify ( requestMsg ) ; } private handleWrapperRequest ( requestMsg ) { if ( requestMsg . command ) { switch ( requestMsg . command ) { case "initialize" : this . initializeArgs = requestMsg . arguments ; this . writeResponseMessage ( { request_seq : requestMsg . seq , success : true , command : requestMsg . command , body : { exceptionBreakpointFilters : [ { default : false , label : "Break On All Exceptions" , filter : "all" , } , ] , supportsLoadedSourcesRequest : false , supportTerminateDebuggee : false , supportsExceptionOptions : true , supportsModulesRequest : false , supportsHitConditionalBreakpoints : false , supportsConfigurationDoneRequest : true , supportsDelayedStackTraceLoading : true , supportsSetVariable : true , supportsGotoTargetsRequest : false , supportsExceptionInfoRequest : false , supportsValueFormattingOptions : true , supportsEvaluateForHovers : true , supportsRestartRequest : false , supportsConditionalBreakpoints : true , supportsStepBack : false , supportsCompletionsRequest : true , supportsRestartFrame : false , supportsStepInTargetsRequest : false , supportsTerminateThreadsRequest : true , } , } ) ; break ; case "disconnect" : this . writeResponseMessage ( { request_seq : requestMsg . seq , success : true , command : requestMsg . command , } ) ; process . exit ( 0 ) ; return true ; case "launch" : this . launchTarget ( < DebugProtocol . LaunchRequest > requestMsg ) ; return true ; case "attach" : this . attachTarget ( < DebugProtocol . AttachRequest > requestMsg ) ; return true ; case "pause" : { this . asyncBreakPending = true ; return false ; } default : } } if ( ! this . debugging ) { this . bufferedRequests . push ( requestMsg ) ; return true ; } return false ; } private processDebuggerMessage ( chunk ) { this . currentOutputData += chunk . toString ( ) ; let idx = this . currentOutputData . indexOf ( "\0" ) ; while ( idx > 0 ) { const message = this . currentOutputData . substr ( 0 , idx ) ; try { const obj = JSON . parse ( message ) ; obj . seq = ++ this . sequenceNumber ; this . writeOutputWithHeader ( obj ) ; } catch ( e ) { process . stderr . write ( `Error parsing message from target: ${ e . toString ( ) }: ${ message }\n` ) ; } this . currentOutputData = this . currentOutputData . substr ( idx + 1 ) ; idx = this . currentOutputData . indexOf ( "\0" ) ; } } private readContentHeader ( ) { const idx = this . currentInputData . indexOf ( TWO_CRLF ) ; if ( idx <= 0 ) { return ; } const header = this . currentInputData . substr ( 0 , idx ) ; const match = header . match ( CONTENT_LENGTH_PATTERN ) ; if ( ! match ) { throw new Error ( "Unable to parse message from debugger client" ) ; } this . currentContentLength = parseInt ( match [ 1 ] , 10 ) ; this . currentInputData = this . currentInputData . substr ( idx + TWO_CRLF . length ) ; ++ this . sequenceNumber ; } private writeOutputEvent ( eventType , message ) { const outputEvent = { seq : ++ this . sequenceNumber , type : "event" , event : "output" , body : { category : eventType , output : message , } , } ; this . writeOutputWithHeader ( outputEvent ) ; } private writeResponseMessage ( message ) { this . writeOutputWithHeader ( { seq : ++ this . sequenceNumber , type : "response" , ... message , } ) ; } private applyCompatabilityFixes ( message ) { if ( message . type === "response" ) { switch ( message . command ) { case "threads" : { if ( Array . isArray ( message . body ) ) { message . body = { threads : message . body } ; } break ; } case "stackTrace" : { message . body . stackFrames . forEach ( ( stackFrame ) => { if ( stackFrame . source != null ) { if ( stackFrame . source . path === "<unknown>" ) { delete stackFrame . source ; } } } ) ; break ; } case "variables" : { message . body . variables . forEach ( ( variable ) => { if ( ! ( "variablesReference" in variable ) ) { variable . variablesReference = 0 ; } } ) ; break ; } default : } } else if ( message . type === "event" ) { switch ( message . event ) { case "output" : { if ( message . body != null && ! message . body . output . endsWith ( "\n" ) ) { message . body . output += "\n" ; } switch ( message . body . category ) { case "warning" : message . body . category = "console" ; break ; case "error" : message . body . category = "stderr" ; break ; case "stdout" : case "info" : message . body . category = "log" ; break ; default : } break ; } case "stopped" : { const reason = ( message . body . reason || "" ) . toLowerCase ( ) . split ( " " ) [ 0 ] ; const focusThread = message . body . threadCausedFocus != null ? message . body . threadCausedFocus : reason === "step" || reason === "breakpoint" || reason === "exception" ; if ( message . body . preserveFocusHint == null ) { message . body . preserveFocusHint = ! focusThread ; } break ; } default : } } } private writeOutputWithHeader ( message ) { this . applyCompatabilityFixes ( message ) ; if ( message . type === "event" && message . event === "breakpoint" ) { if ( message . body . reason !== "changed" ) { return ; } message . body . breakpoint . column = 1 ; } if ( message . type === "event" && message . event === "stopped" ) { if ( ! this . nonLoaderBreakSeen ) { if ( message . body != null && message . body . description !== "execution paused" ) { this . nonLoaderBreakSeen = true ; } else if ( ! this . asyncBreakPending ) { return ; } } this . asyncBreakPending = false ; } if ( message . type === "event" && message . event === "stopped" && ( message . body . threadCausedFocus === false || message . body . preserveFocusHint === true ) && this . initializeArgs . clientID !== "atom" ) { return ; } let output = JSON . stringify ( message ) ; if ( this . remoteSiteRootPattern && this . localWorkspaceRoot ) { output = output . replace ( this . remoteSiteRootPattern , this . localWorkspaceRoot ) ; } const length = Buffer . byteLength ( output , "utf8" ) ; process . stdout . write ( `Content-Length: ${ length }${ TWO_CRLF }` , "utf8" ) ; process . stdout . write ( output , "utf8" ) ; } private escapeRegExp ( str ) { return str . replace ( /[.*+?^${}()|[\]\\]/g , "\\$&" ) ; } } new HHVMDebuggerWrapper ( ) . debug ( ) ;