import * as _ from 'lodash' ; import * as path from 'path' ; import { Interceptor } from '.' ; import { HtkConfig } from '../config' ; import { spawnToResult , waitForExit } from '../util/process-management' ; import { OVERRIDE_JAVA_AGENT } from './terminal/terminal-env-overrides' ; import { reportError } from '../error-tracking' ; import { delay } from '../util/promise' ; import { commandExists , canAccess } from '../util/fs' ; type JvmTarget = { pid : string ; name : string ; interceptedByProxy : number | undefined ; } ; const javaBinPromise = ( async ( ) => { const javaBinPaths = [ ! ! process . env . JAVA_HOME && path . join ( process . env . JAVA_HOME ! ! , 'bin' , 'java' ) , await getMacJavaHome ( ) , ( await commandExists ( 'java' ) ) && process . platform !== "darwin" && 'java' ] . filter ( p => ! ! p ) as string [ ] ; const javaTestResults = await Promise . all ( javaBinPaths . map ( async ( possibleJavaBin ) => ( { javaBin : possibleJavaBin , output : await testJavaBin ( possibleJavaBin ) . catch ( ( e ) => ( { exitCode : - 1 , stdout : '' , stderr : e . toString ( ) } ) ) } ) ) ) ; const bestJava = javaTestResults . filter ( ( { output } ) => output . exitCode === 0 ) [ 0 ] ; if ( javaTestResults . length && ! bestJava ) { javaTestResults . forEach ( ( testResult ) => { console . log ( `Running ${ testResult . javaBin }:` ) ; console . log ( testResult . output . stdout ) ; console . log ( testResult . output . stderr ) ; } ) ; throw new Error ( `JVM attach not available, exited with ${ javaTestResults [ 0 ] . output . exitCode }` ) ; } else if ( bestJava ) { return bestJava . javaBin ; } else { return false ; } } ) ( ) . catch ( ( e ) => { reportError ( e ) ; return false ; } ) ; async function getMacJavaHome ( ) { if ( ! await canAccess ( '/usr/libexec/java_home' ) ) return ; const result = await spawnToResult ( '/usr/libexec/java_home' , [ '-v' , '1.9+' ] ) ; if ( result . exitCode != 0 ) return ; else return path . join ( result . stdout . trim ( ) , 'bin' , 'java' ) ; } function testJavaBin ( possibleJavaBin ) { return Promise . race ( [ spawnToResult ( possibleJavaBin , [ '-Djdk.attach.allowAttachSelf=true' , '-jar' , OVERRIDE_JAVA_AGENT , 'self-test' ] ) , delay ( 30000 ) . then ( ( ) => { throw new Error ( `Java bin test for ${ possibleJavaBin } timed out` ) ; } ) ] ) ; } export class JvmInterceptor implements Interceptor { readonly id = 'attach-jvm' ; readonly version = '1.0.1' ; private interceptedProcesses : { [ pid ] : number ; } = { } ; constructor ( private config ) { } async isActivable ( ) { return ! ! await javaBinPromise ; } activableTimeout = 2000 ; isActive ( proxyPort ) { return _ . some ( this . interceptedProcesses , ( port ) => port === proxyPort ) ; } async getMetadata ( type ) { if ( type === 'summary' ) return { } ; if ( ! this . targetsPromise ) { this . targetsPromise = this . getTargets ( ) . finally ( ( ) => { this . targetsPromise = undefined ; } ) ; } const targets = await this . targetsPromise ; return { jvmTargets : _ . keyBy ( targets , 'pid' ) } ; } private targetsPromise : Promise < JvmTarget [ ] > | undefined ; private async getTargets ( ) { const javaBin = await javaBinPromise ; if ( ! javaBin ) throw new Error ( "Attach activated but no Java available" ) ; const listTargetsOutput = await spawnToResult ( javaBin , [ '-jar' , OVERRIDE_JAVA_AGENT , 'list-targets' ] ) ; if ( listTargetsOutput . exitCode !== 0 ) { reportError ( `JVM target lookup failed with status ${ listTargetsOutput . exitCode }` ) ; return [ ] ; } return listTargetsOutput . stdout . split ( '\n' ) . filter ( line => line . includes ( ':' ) ) . map ( ( line ) => { const nameIndex = line . indexOf ( ':' ) + 1 ; const pid = line . substring ( 0 , nameIndex - 1 ) ; return { pid , name : line . substring ( nameIndex ) , interceptedByProxy : this . interceptedProcesses [ pid ] } ; } ) . filter ( ( target ) => ! target . name . includes ( OVERRIDE_JAVA_AGENT ) ) ; } async activate ( proxyPort , options ) { const interceptionResult = await spawnToResult ( 'java' , [ '-jar' , OVERRIDE_JAVA_AGENT , options . targetPid , '127.0.0.1' , proxyPort . toString ( ) , this . config . https . certPath ] , { } ) ; if ( interceptionResult . exitCode !== 0 ) { console . log ( interceptionResult . stdout ) ; console . log ( interceptionResult . stderr ) ; throw new Error ( `Failed to attach to JVM, exit code ${ interceptionResult . exitCode }` ) ; } else { this . interceptedProcesses [ options . targetPid ] = proxyPort ; waitForExit ( parseInt ( options . targetPid , 10 ) , Infinity ) . then ( ( ) => { delete this . interceptedProcesses [ options . targetPid ] ; } ) ; } } async deactivate ( proxyPort ) { } async deactivateAll ( ) { } }