1+ import * as _ from 'lodash' ;
2+
3+ import { Interceptor } from '.' ;
4+
5+ import { HtkConfig } from '../config' ;
6+ import { spawnToResult , waitForExit } from '../process-management' ;
7+ import { OVERRIDE_JAVA_AGENT } from './terminal/terminal-env-overrides' ;
8+ import { reportError } from '../error-tracking' ;
9+ import { commandExists } from '../util' ;
10+
11+ type JvmTarget = { pid : string , name : string , interceptedByProxy : number | undefined } ;
12+
13+ // Check that Java is present, and that it's possible to run list-targets
14+ const isJavaAvailable = commandExists ( 'java' ) . then ( async ( isAvailable ) => {
15+ if ( ! isAvailable ) return false ;
16+
17+ const result = await spawnToResult (
18+ 'java' , [
19+ '-jar' , OVERRIDE_JAVA_AGENT ,
20+ 'list-targets'
21+ ]
22+ ) ;
23+
24+ return result . exitCode === 0 ;
25+ } ) . catch ( ( e ) => {
26+ // This is expected to happen occasionally, e.g. when using Java 8 (which doesn't support
27+ // the VM attachment APIs we need).
28+ console . log ( "Error checking for JVM targets" , e ) ;
29+ return false ;
30+ } ) ;
31+
32+ export class JvmInterceptor implements Interceptor {
33+ readonly id = 'attach-jvm' ;
34+ readonly version = '1.0.0' ;
35+
36+ private interceptedProcesses : {
37+ [ pid : string ] : number // PID -> proxy port
38+ } = { } ;
39+
40+ constructor ( private config : HtkConfig ) { }
41+
42+ async isActivable ( ) : Promise < boolean > {
43+ return await isJavaAvailable ;
44+ }
45+
46+ isActive ( proxyPort : number | string ) {
47+ return _ . some ( this . interceptedProcesses , ( port ) => port === proxyPort ) ;
48+ }
49+
50+ async getMetadata ( type : 'summary' | 'detailed' ) : Promise < {
51+ jvmTargets ?: { [ pid : string ] : JvmTarget }
52+ } > {
53+ // We only poll the targets available when explicitly requested,
54+ // since it's a bit expensive.
55+ if ( type === 'summary' ) return { } ;
56+
57+ const listTargetsOutput = await spawnToResult (
58+ 'java' , [
59+ '-jar' , OVERRIDE_JAVA_AGENT ,
60+ 'list-targets'
61+ ]
62+ ) ;
63+
64+ if ( listTargetsOutput . exitCode !== 0 ) {
65+ reportError ( `JVM target lookup failed with status ${ listTargetsOutput . exitCode } ` ) ;
66+ return { jvmTargets : { } } ;
67+ }
68+
69+ const targets = listTargetsOutput . stdout
70+ . split ( '\n' )
71+ . filter ( line => line . includes ( ':' ) )
72+ . map ( ( line ) => {
73+ const nameIndex = line . indexOf ( ':' ) + 1 ;
74+
75+ const pid = line . substring ( 0 , nameIndex - 1 ) ;
76+
77+ return {
78+ pid,
79+ name : line . substring ( nameIndex ) ,
80+ interceptedByProxy : this . interceptedProcesses [ pid ]
81+ } ;
82+ } )
83+ . filter ( ( target ) =>
84+ // Exclude our own attacher and/or list-target queries from this list
85+ ! target . name . includes ( `-jar ${ OVERRIDE_JAVA_AGENT } ` )
86+ ) ;
87+
88+ return {
89+ jvmTargets : _ . keyBy ( targets , 'pid' )
90+ } ;
91+ }
92+
93+ async activate ( proxyPort : number , options : {
94+ targetPid : string
95+ } ) : Promise < void > {
96+ const interceptionResult = await spawnToResult (
97+ 'java' , [
98+ '-jar' , OVERRIDE_JAVA_AGENT ,
99+ options . targetPid ,
100+ '127.0.0.1' ,
101+ proxyPort . toString ( ) ,
102+ this . config . https . certPath
103+ ] ,
104+ { } ,
105+ true // Inherit IO, so we can see output easily, if any
106+ ) ;
107+
108+ if ( interceptionResult . exitCode !== 0 ) {
109+ throw new Error ( `Failed to attach to JVM, exit code ${ interceptionResult . exitCode } ` ) ;
110+ } else {
111+ this . interceptedProcesses [ options . targetPid ] = proxyPort ;
112+
113+ // Poll the status of this pid every 250ms - remove it once it disappears.
114+ waitForExit ( parseInt ( options . targetPid , 10 ) , Infinity )
115+ . then ( ( ) => {
116+ delete this . interceptedProcesses [ options . targetPid ] ;
117+ } ) ;
118+ }
119+ }
120+
121+ // Nothing we can do to deactivate, unfortunately. In theory the agent could do this, unwriting all
122+ // it's changes, but it's *super* complicated to do for limited benefit.
123+ async deactivate ( proxyPort : number | string ) : Promise < void > { }
124+ async deactivateAll ( ) : Promise < void > { }
125+
126+ }
0 commit comments