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