import * as fs from "fs" ; import * as _ from "lodash" ; import * as os from "os" ; import * as path from "path" ; import * as vscode from "vscode" ; import * as dotenv from 'dotenv' ; import { instrumentOperation , sendError , sendInfo , setUserError } from "vscode-extension-telemetry-wrapper" ; import * as anchor from "./anchor" ; import { buildWorkspace } from "./build" ; import { populateStepFilters , substituteFilterVariables } from "./classFilter" ; import * as commands from "./commands" ; import { ClasspathVariable } from "./constants" ; import { Type } from "./javaLogger" ; import * as lsPlugin from "./languageServerPlugin" ; import { addMoreHelpfulVMArgs , getJavaVersion , getShortenApproachForCLI , validateRuntimeCompatibility } from "./launchCommand" ; import { mainClassPicker } from "./mainClassPicker" ; import { resolveJavaProcess } from "./processPicker" ; import { IProgressReporter } from "./progressAPI" ; import { progressProvider } from "./progressImpl" ; import * as utility from "./utility" ; const platformNameMappings = { win32 : "windows" , linux : "linux" , darwin : "osx" , } ; const platformName = platformNameMappings [ process . platform ] ; export class JavaDebugConfigurationProvider implements vscode . DebugConfigurationProvider { private isUserSettingsDirty : boolean = true ; constructor ( ) { vscode . workspace . onDidChangeConfiguration ( ( event ) => { if ( event . affectsConfiguration ( "java.debug" ) ) { if ( vscode . debug . activeDebugSession ) { this . isUserSettingsDirty = false ; return updateDebugSettings ( event ) ; } else { this . isUserSettingsDirty = true ; } } return undefined ; } ) ; } public provideDebugConfigurations ( folder , token ? ) { const provideDebugConfigurationsHandler = instrumentOperation ( "provideDebugConfigurations" , ( _operationId ) => { return < Thenable < vscode . DebugConfiguration [ ] > > this . provideDebugConfigurationsAsync ( folder , token ) ; } ) ; return provideDebugConfigurationsHandler ( ) ; } public resolveDebugConfiguration ( _folder , config , _token ? ) { if ( this . isEmptyConfig ( config ) ) { config . type = "java" ; config . name = "Java Debug" ; config . request = "launch" ; } return config ; } public resolveDebugConfigurationWithSubstitutedVariables ( folder , config , token ? ) { const resolveDebugConfigurationHandler = instrumentOperation ( "resolveDebugConfiguration" , ( _operationId ) => { try { this . mergePlatformProperties ( config , folder ) ; return this . resolveAndValidateDebugConfiguration ( folder , config , token ) ; } catch ( ex ) { utility . showErrorMessage ( { type : Type . EXCEPTION , message : String ( ( ex && ex . message ) || ex ) , } ) ; return undefined ; } } ) ; return resolveDebugConfigurationHandler ( ) ; } private provideDebugConfigurationsAsync ( folder , token ? ) { return new Promise ( async ( resolve , _reject ) => { const progressReporter = progressProvider . createProgressReporter ( "Create launch.json" , vscode . ProgressLocation . Window ) ; progressReporter . observe ( token ) ; const defaultLaunchConfig = { type : "java" , name : "Launch Current File" , request : "launch" , mainClass : "${file}" , } ; try { const isOnStandardMode = await utility . waitForStandardMode ( progressReporter ) ; if ( ! isOnStandardMode ) { resolve ( [ defaultLaunchConfig ] ) ; return ; } if ( progressReporter . isCancelled ( ) ) { resolve ( [ defaultLaunchConfig ] ) ; return ; } progressReporter . report ( "Generating Java configuration..." ) ; const mainClasses = await lsPlugin . resolveMainClass ( folder ? folder . uri : undefined ) ; const cache = { } ; const launchConfigs = mainClasses . map ( ( item ) => { return { ... defaultLaunchConfig , name : this . constructLaunchConfigName ( item . mainClass , cache ) , mainClass : item . mainClass , projectName : item . projectName , } ; } ) ; if ( progressReporter . isCancelled ( ) ) { resolve ( [ defaultLaunchConfig ] ) ; return ; } resolve ( [ defaultLaunchConfig , ... launchConfigs ] ) ; } catch ( ex ) { if ( ex instanceof utility . JavaExtensionNotEnabledError ) { utility . guideToInstallJavaExtension ( ) ; } else { console . error ( ex ) ; } resolve ( [ defaultLaunchConfig ] ) ; } finally { progressReporter . done ( ) ; } } ) ; } private mergePlatformProperties ( config , _folder ? ) { if ( config && platformName && config [ platformName ] ) { try { for ( const key of Object . keys ( config [ platformName ] ) ) { config [ key ] = config [ platformName ] [ key ] ; } config [ platformName ] = undefined ; } catch { } } } private constructLaunchConfigName ( mainClass , cache ) { const name = `Launch ${ mainClass . substr ( mainClass . lastIndexOf ( "." ) + 1 ) }` ; if ( cache [ name ] === undefined ) { cache [ name ] = 0 ; return name ; } else { cache [ name ] += 1 ; return `${ name }(${ cache [ name ] })` ; } } private mergeEnvFile ( config ) { const baseEnv = config . env || { } ; let result = baseEnv ; if ( config . envFile ) { try { result = { ... baseEnv , ... readEnvFile ( config . envFile ) , } ; } catch ( e ) { throw new utility . UserError ( { message : "Cannot load environment file." , type : Type . USAGEERROR , } ) ; } } config . env = result ; } private async resolveAndValidateDebugConfiguration ( folder , config , token ? ) { let progressReporter = progressProvider . getProgressReporter ( config . __progressId ) ; if ( ! progressReporter && config . __progressId ) { return undefined ; } else if ( ! progressReporter ) { progressReporter = progressProvider . createProgressReporter ( config . noDebug ? "Run" : "Debug" ) ; } progressReporter . observe ( token ) ; if ( progressReporter . isCancelled ( ) ) { return undefined ; } try { const isOnStandardMode = await utility . waitForStandardMode ( progressReporter ) ; if ( ! isOnStandardMode || progressReporter . isCancelled ( ) ) { return undefined ; } if ( this . isUserSettingsDirty ) { this . isUserSettingsDirty = false ; await updateDebugSettings ( ) ; } if ( this . isEmptyConfig ( config ) ) { config . type = "java" ; config . name = "Java Debug" ; config . request = "launch" ; } if ( config . request === "launch" ) { this . mergeEnvFile ( config ) ; if ( config . vmArgs === undefined ) { const debugSettings = vscode . workspace . getConfiguration ( "java.debug.settings" ) ; config . vmArgs = debugSettings . vmArgs ; } if ( ! config . console ) { const debugSettings = vscode . workspace . getConfiguration ( "java.debug.settings" ) ; config . console = debugSettings . console ; } if ( config . console === "integratedTerminal" && ! config . internalConsoleOptions ) { config . internalConsoleOptions = "neverOpen" ; } if ( needsBuildWorkspace ( ) ) { progressReporter . report ( "Compiling..." ) ; const proceed = await buildWorkspace ( progressReporter ) ; if ( ! proceed ) { return undefined ; } } if ( progressReporter . isCancelled ( ) ) { return undefined ; } if ( ! config . mainClass ) { progressReporter . report ( "Resolving main class..." ) ; } else { progressReporter . report ( "Resolving launch configuration..." ) ; } const mainClassOption = await this . resolveAndValidateMainClass ( folder && folder . uri , config , progressReporter ) ; if ( ! mainClassOption || ! mainClassOption . mainClass ) { return undefined ; } progressReporter . report ( "Resolving launch configuration..." ) ; config . mainClass = mainClassOption . mainClass ; config . projectName = mainClassOption . projectName ; if ( progressReporter . isCancelled ( ) ) { return undefined ; } if ( _ . isEmpty ( config . classPaths ) && _ . isEmpty ( config . modulePaths ) ) { const result = < any [ ] > ( await lsPlugin . resolveClasspath ( config . mainClass , config . projectName ) ) ; config . modulePaths = result [ 0 ] ; config . classPaths = result [ 1 ] ; } else { config . modulePaths = await this . resolvePath ( folder , config . modulePaths , config . mainClass , config . projectName , true ) ; config . classPaths = await this . resolvePath ( folder , config . classPaths , config . mainClass , config . projectName , false ) ; } if ( _ . isEmpty ( config . classPaths ) && _ . isEmpty ( config . modulePaths ) ) { throw new utility . UserError ( { message : "Cannot resolve the modulepaths/classpaths automatically, please specify the value in the launch.json." , type : Type . USAGEERROR , } ) ; } config . javaExec = await lsPlugin . resolveJavaExecutable ( config . mainClass , config . projectName ) ; config . cwd = config . cwd || _ . get ( folder , "uri.fsPath" ) ; if ( Array . isArray ( config . args ) ) { config . args = this . concatArgs ( config . args ) ; } if ( Array . isArray ( config . vmArgs ) ) { config . vmArgs = this . concatArgs ( config . vmArgs ) ; } if ( progressReporter . isCancelled ( ) ) { return undefined ; } await populateStepFilters ( config ) ; const targetJavaVersion = await getJavaVersion ( config . javaExec ) ; if ( await lsPlugin . detectPreviewFlag ( config . mainClass , config . projectName ) ) { config . vmArgs = ( config . vmArgs || "" ) + " --enable-preview" ; validateRuntimeCompatibility ( targetJavaVersion ) ; } await addMoreHelpfulVMArgs ( config , targetJavaVersion ) ; if ( ! config . shortenCommandLine || config . shortenCommandLine === "auto" ) { config . shortenCommandLine = await getShortenApproachForCLI ( config , targetJavaVersion ) ; } if ( config . console === "internalConsole" && ! config . encoding ) { config . encoding = "UTF-8" ; } } else if ( config . request === "attach" ) { if ( config . hostName && config . port && Number . isInteger ( Number ( config . port ) ) ) { config . port = Number ( config . port ) ; config . processId = undefined ; } else if ( config . processId !== undefined ) { if ( config . processId === "${command:PickJavaProcess}" ) { return undefined ; } const pid = Number ( config . processId ) ; if ( Number . isNaN ( pid ) ) { vscode . window . showErrorMessage ( `The processId config '${ config . processId }' is not a valid process id.` ) ; return undefined ; } const javaProcess = await resolveJavaProcess ( pid ) ; if ( ! javaProcess ) { vscode . window . showErrorMessage ( `Attach to process: pid '${ config . processId }' is not a debuggable Java process. ` + `Please make sure the process has turned on debug mode using vmArgs like ` + `'-agentlib:jdwp=transport=dt_socket,server=y,address=5005.'` ) ; return undefined ; } config . processId = undefined ; config . hostName = javaProcess . hostName ; config . port = javaProcess . debugPort ; } else { throw new utility . UserError ( { message : "Please specify the hostName/port directly, or provide the processId of the remote debuggee in the launch.json." , type : Type . USAGEERROR , anchor : anchor . ATTACH_CONFIG_ERROR , } ) ; } await populateStepFilters ( config ) ; } else { throw new utility . UserError ( { message : `Request type "${ config . request }" is not supported. Only "launch" and "attach" are supported.` , type : Type . USAGEERROR , anchor : anchor . REQUEST_TYPE_NOT_SUPPORTED , } ) ; } if ( token ?. isCancellationRequested || progressReporter . isCancelled ( ) ) { return undefined ; } delete config . __progressId ; return config ; } catch ( ex ) { if ( ex instanceof utility . JavaExtensionNotEnabledError ) { utility . guideToInstallJavaExtension ( ) ; return undefined ; } if ( ex instanceof utility . UserError ) { utility . showErrorMessageWithTroubleshooting ( ex . context ) ; return undefined ; } utility . showErrorMessageWithTroubleshooting ( utility . convertErrorToMessage ( ex ) ) ; return undefined ; } finally { progressReporter . done ( ) ; } } private async resolvePath ( folder , pathArray , mainClass , projectName , isModulePath ) { if ( _ . isEmpty ( pathArray ) ) { return [ ] ; } const pathVariables = [ ClasspathVariable . Auto , ClasspathVariable . Runtime , ClasspathVariable . Test ] ; const containedVariables = pathArray . filter ( ( cp ) => pathVariables . includes ( cp ) ) ; if ( _ . isEmpty ( containedVariables ) ) { return this . filterExcluded ( folder , pathArray ) ; } const scope = this . mergeScope ( containedVariables ) ; const response = < any [ ] > await lsPlugin . resolveClasspath ( mainClass , projectName , scope ) ; const resolvedPaths = isModulePath ? response ?. [ 0 ] : response ?. [ 1 ] ; if ( ! resolvedPaths ) { console . log ( "The Java Language Server failed to resolve the classpaths/modulepaths" ) ; } const paths = [ ] ; let replaced = false ; for ( const p of pathArray ) { if ( pathVariables . includes ( p ) ) { if ( ! replaced ) { paths . push ( ... resolvedPaths ) ; replaced = true ; } continue ; } paths . push ( p ) ; } return this . filterExcluded ( folder , paths ) ; } private async filterExcluded ( folder , paths ) { const result = [ ] ; const excludes = new Map < string , boolean > ( ) ; for ( const p of paths ) { if ( p . startsWith ( "!" ) ) { let exclude = p . substr ( 1 ) ; if ( ! path . isAbsolute ( exclude ) ) { exclude = path . join ( folder ?. uri . fsPath || "" , exclude ) ; } excludes . set ( vscode . Uri . file ( exclude ) . fsPath , this . isFile ( exclude ) ) ; continue ; } result . push ( vscode . Uri . file ( p ) . fsPath ) ; } return result . filter ( ( r ) => { for ( const [ excludedPath , isFile ] of excludes . entries ( ) ) { if ( isFile && r === excludedPath ) { return false ; } if ( ! isFile && r . startsWith ( excludedPath ) ) { return false ; } } return true ; } ) ; } private mergeScope ( scopes ) { if ( scopes . includes ( ClasspathVariable . Test ) ) { return "test" ; } if ( scopes . includes ( ClasspathVariable . Auto ) ) { return undefined ; } if ( scopes . includes ( ClasspathVariable . Runtime ) ) { return "runtime" ; } return undefined ; } /**
     * Converts an array of arguments to a string as the args and vmArgs.
     */ private concatArgs ( args ) { return _ . join ( _ . map ( args , ( arg ) : string => { const str = String ( arg ) ; if ( /["\s]/ . test ( str ) ) { return "\"" + str . replace ( /(["\\])/g , "\\$1" ) + "\"" ; } return str ; } ) , " " ) ; } /**
     * When VS Code cannot find any available DebugConfiguration, it passes a { noDebug?: boolean } to resolve.
     * This function judges whether a DebugConfiguration is empty by filtering out the field "noDebug".
     */ private isEmptyConfig ( config ) { return Object . keys ( config ) . filter ( ( key ) => key !== "noDebug" ) . length === 0 ; } private async resolveAndValidateMainClass ( folder , config , progressReporter ) { if ( ! config . mainClass || this . isFile ( config . mainClass ) ) { const currentFile = config . mainClass || _ . get ( vscode . window . activeTextEditor , "document.uri.fsPath" ) ; if ( currentFile ) { const mainEntries = await lsPlugin . resolveMainMethod ( vscode . Uri . file ( currentFile ) ) ; if ( progressReporter . isCancelled ( ) ) { return undefined ; } else if ( mainEntries . length ) { if ( ! mainClassPicker . isAutoPicked ( mainEntries ) ) { progressReporter . hide ( true ) ; } return mainClassPicker . showQuickPick ( mainEntries , "Please select a main class you want to run." ) ; } } const hintMessage = currentFile ? `The file '${ path . basename ( currentFile ) }' is not executable, please select a main class you want to run.` : "Please select a main class you want to run." ; return this . promptMainClass ( folder , progressReporter , hintMessage ) ; } const containsExternalClasspaths = ! _ . isEmpty ( config . classPaths ) || ! _ . isEmpty ( config . modulePaths ) ; const validationResponse = await lsPlugin . validateLaunchConfig ( config . mainClass , config . projectName , containsExternalClasspaths , folder ) ; if ( progressReporter . isCancelled ( ) ) { return undefined ; } else if ( ! validationResponse . mainClass . isValid || ! validationResponse . projectName . isValid ) { return this . fixMainClass ( folder , config , validationResponse , progressReporter ) ; } return { mainClass : config . mainClass , projectName : config . projectName , } ; } private isFile ( filePath ) { try { return fs . lstatSync ( filePath ) . isFile ( ) ; } catch ( error ) { return false ; } } private getValidationErrorMessage ( error ) { switch ( error . kind ) { case lsPlugin . CONFIGERROR_INVALID_CLASS_NAME : return "ConfigError: mainClass was configured with an invalid class name." ; case lsPlugin . CONFIGERROR_MAIN_CLASS_NOT_EXIST : return "ConfigError: mainClass does not exist." ; case lsPlugin . CONFIGERROR_MAIN_CLASS_NOT_UNIQUE : return "ConfigError: mainClass is not unique in the workspace" ; case lsPlugin . CONFIGERROR_INVALID_JAVA_PROJECT : return "ConfigError: could not find a Java project with the configured projectName." ; } return "ConfigError: Invalid mainClass/projectName configs." ; } private async fixMainClass ( folder , config , validationResponse , progressReporter ) { const errors = [ ] ; if ( ! validationResponse . mainClass . isValid ) { errors . push ( String ( validationResponse . mainClass . message ) ) ; const errorLog = { name : "error" , message : this . getValidationErrorMessage ( validationResponse . mainClass ) , } ; setUserError ( errorLog ) ; sendError ( errorLog ) ; } if ( ! validationResponse . projectName . isValid ) { errors . push ( String ( validationResponse . projectName . message ) ) ; const errorLog = { name : "error" , message : this . getValidationErrorMessage ( validationResponse . projectName ) , } ; setUserError ( errorLog ) ; sendError ( errorLog ) ; } if ( validationResponse . proposals && validationResponse . proposals . length ) { progressReporter . hide ( true ) ; const answer = await utility . showErrorMessageWithTroubleshooting ( { message : errors . join ( os . EOL ) , type : Type . USAGEERROR , anchor : anchor . FAILED_TO_RESOLVE_CLASSPATH , bypassLog : true , } , "Fix" ) ; if ( answer === "Fix" ) { const selectedFix = await mainClassPicker . showQuickPick ( validationResponse . proposals , "Please select main class<project name>." , false ) ; if ( selectedFix ) { sendInfo ( "" , { fix : "yes" , fixMessage : "Fix the configs of mainClass and projectName" , } ) ; await this . persistMainClassOption ( folder , config , selectedFix ) ; } return selectedFix ; } return undefined ; } throw new utility . UserError ( { message : errors . join ( os . EOL ) , type : Type . USAGEERROR , anchor : anchor . FAILED_TO_RESOLVE_CLASSPATH , bypassLog : true , } ) ; } private async persistMainClassOption ( folder , oldConfig , change ) { const newConfig = _ . cloneDeep ( oldConfig ) ; newConfig . mainClass = change . mainClass ; newConfig . projectName = change . projectName ; return this . persistLaunchConfig ( folder , oldConfig , newConfig ) ; } private async persistLaunchConfig ( folder , oldConfig , newConfig ) { const launchConfigurations = vscode . workspace . getConfiguration ( "launch" , folder ) ; const rawConfigs = launchConfigurations . configurations ; const targetIndex = _ . findIndex ( rawConfigs , ( config ) => _ . isEqual ( config , oldConfig ) ) ; if ( targetIndex >= 0 ) { rawConfigs [ targetIndex ] = newConfig ; await launchConfigurations . update ( "configurations" , rawConfigs ) ; } } private async promptMainClass ( folder , progressReporter , hintMessage ? ) { const res = await lsPlugin . resolveMainClass ( folder ) ; if ( progressReporter . isCancelled ( ) ) { return undefined ; } else if ( res . length === 0 ) { const workspaceFolder = folder ? vscode . workspace . getWorkspaceFolder ( folder ) : undefined ; throw new utility . UserError ( { message : `Cannot find a class with the main method${ workspaceFolder ? " in the folder '" + workspaceFolder . name + "'" : "" }.` , type : Type . USAGEERROR , anchor : anchor . CANNOT_FIND_MAIN_CLASS , } ) ; } if ( ! mainClassPicker . isAutoPicked ( res ) ) { progressReporter . hide ( true ) ; } return mainClassPicker . showQuickPickWithRecentlyUsed ( res , hintMessage || "Select main class<project name>" ) ; } } async function updateDebugSettings ( event ? ) { const debugSettingsRoot = vscode . workspace . getConfiguration ( "java.debug" ) ; if ( ! debugSettingsRoot ) { return ; } const logLevel = convertLogLevel ( debugSettingsRoot . logLevel || "" ) ; const javaHome = await utility . getJavaHome ( ) ; if ( debugSettingsRoot . settings && Object . keys ( debugSettingsRoot . settings ) . length ) { try { const stepFilters = { skipClasses : await substituteFilterVariables ( debugSettingsRoot . settings . stepping . skipClasses ) , skipSynthetics : debugSettingsRoot . settings . stepping . skipSynthetics , skipStaticInitializers : debugSettingsRoot . settings . stepping . skipStaticInitializers , skipConstructors : debugSettingsRoot . settings . stepping . skipConstructors , } ; const exceptionFilters = { skipClasses : await substituteFilterVariables ( debugSettingsRoot . settings . exceptionBreakpoint . skipClasses ) , } ; const settings = await commands . executeJavaLanguageServerCommand ( commands . JAVA_UPDATE_DEBUG_SETTINGS , JSON . stringify ( { ... debugSettingsRoot . settings , logLevel , javaHome , stepFilters , exceptionFilters , exceptionFiltersUpdated : event && event . affectsConfiguration ( "java.debug.settings.exceptionBreakpoint.skipClasses" ) , limitOfVariablesPerJdwpRequest : Math . max ( debugSettingsRoot . settings . jdwp . limitOfVariablesPerJdwpRequest , 1 ) , jdwpRequestTimeout : Math . max ( debugSettingsRoot . settings . jdwp . requestTimeout , 100 ) , } ) ) ; if ( logLevel === "FINE" ) { console . log ( "settings:" , settings ) ; } } catch ( err ) { console . log ( "Cannot update debug settings." , err ) ; } } } function needsBuildWorkspace ( ) { const debugSettingsRoot = vscode . workspace . getConfiguration ( "java.debug.settings" ) ; return debugSettingsRoot ? debugSettingsRoot . forceBuildBeforeLaunch : true ; } function convertLogLevel ( commonLogLevel ) { switch ( commonLogLevel . toLowerCase ( ) ) { case "verbose" : return "FINE" ; case "warn" : return "WARNING" ; case "error" : return "SEVERE" ; case "info" : return "INFO" ; default : return "FINE" ; } } function readEnvFile ( file ) { if ( ! fs . existsSync ( file ) ) { return { } ; } const buffer = stripBOM ( fs . readFileSync ( file , "utf8" ) ) ; const env = dotenv . parse ( Buffer . from ( buffer ) ) ; return env ; } function stripBOM ( s ) { if ( s && s [ 0 ] === "\uFEFF" ) { s = s . substr ( 1 ) ; } return s ; }