99
1010package org .elasticsearch .entitlement .instrumentation .impl ;
1111
12+ import org .elasticsearch .common .Strings ;
1213import org .elasticsearch .entitlement .api .EntitlementChecks ;
1314import org .elasticsearch .entitlement .api .EntitlementProvider ;
1415import org .elasticsearch .entitlement .instrumentation .InstrumentationService ;
15- import org .elasticsearch .entitlement .instrumentation .MethodKey ;
1616import org .elasticsearch .logging .LogManager ;
1717import org .elasticsearch .logging .Logger ;
1818import org .elasticsearch .test .ESTestCase ;
1919import org .junit .Before ;
20+ import org .objectweb .asm .Type ;
2021
2122import java .lang .reflect .InvocationTargetException ;
2223import java .lang .reflect .Method ;
23- import java .util .Map ;
24+ import java .util .Arrays ;
25+ import java .util .stream .Collectors ;
2426
2527import static org .elasticsearch .entitlement .instrumentation .impl .ASMUtils .bytecode2text ;
28+ import static org .elasticsearch .entitlement .instrumentation .impl .InstrumenterImpl .getClassFileInfo ;
29+ import static org .hamcrest .Matchers .is ;
2630
2731/**
2832 * This tests {@link InstrumenterImpl} in isolation, without a java agent.
@@ -60,6 +64,10 @@ public static class ClassToInstrument implements Testable {
6064 public static void systemExit (int status ) {
6165 assertEquals (123 , status );
6266 }
67+
68+ public static void anotherSystemExit (int status ) {
69+ assertEquals (123 , status );
70+ }
6371 }
6472
6573 static final class TestException extends RuntimeException {}
@@ -76,8 +84,11 @@ public static class TestEntitlementManager implements EntitlementChecks {
7684 */
7785 volatile boolean isActive ;
7886
87+ int checkSystemExitCallCount = 0 ;
88+
7989 @ Override
8090 public void checkSystemExit (Class <?> callerClass , int status ) {
91+ checkSystemExitCallCount ++;
8192 assertSame (InstrumenterTests .class , callerClass );
8293 assertEquals (123 , status );
8394 throwIfActive ();
@@ -90,18 +101,11 @@ private void throwIfActive() {
90101 }
91102 }
92103
93- public void test () throws Exception {
94- // This test doesn't replace ClassToInstrument in-place but instead loads a separate
95- // class ClassToInstrument_NEW that contains the instrumentation. Because of this,
96- // we need to configure the Transformer to use a MethodKey and instrumentationMethod
97- // with slightly different signatures (using the common interface Testable) which
98- // is not what would happen when it's run by the agent.
99-
100- MethodKey k1 = instrumentationService .methodKeyForTarget (ClassToInstrument .class .getMethod ("systemExit" , int .class ));
101- Method v1 = EntitlementChecks .class .getMethod ("checkSystemExit" , Class .class , int .class );
102- var instrumenter = new InstrumenterImpl ("_NEW" , Map .of (k1 , v1 ));
104+ public void testClassIsInstrumented () throws Exception {
105+ var classToInstrument = ClassToInstrument .class ;
106+ var instrumenter = createInstrumenter (classToInstrument , "systemExit" );
103107
104- byte [] newBytecode = instrumenter .instrumentClassFile (ClassToInstrument . class ).bytecodes ();
108+ byte [] newBytecode = instrumenter .instrumentClassFile (classToInstrument ).bytecodes ();
105109
106110 if (logger .isTraceEnabled ()) {
107111 logger .trace ("Bytecode after instrumentation:\n {}" , bytecode2text (newBytecode ));
@@ -112,22 +116,96 @@ public void test() throws Exception {
112116 newBytecode
113117 );
114118
119+ getTestChecks ().isActive = false ;
120+
115121 // Before checking is active, nothing should throw
116- callStaticSystemExit (newClass , 123 );
122+ callStaticMethod (newClass , "systemExit" , 123 );
117123
118124 getTestChecks ().isActive = true ;
119125
120126 // After checking is activated, everything should throw
121- assertThrows (TestException .class , () -> callStaticSystemExit (newClass , 123 ));
127+ assertThrows (TestException .class , () -> callStaticMethod (newClass , "systemExit" , 123 ));
128+ }
129+
130+ public void testClassIsNotInstrumentedTwice () throws Exception {
131+ var classToInstrument = ClassToInstrument .class ;
132+ var instrumenter = createInstrumenter (classToInstrument , "systemExit" );
133+
134+ InstrumenterImpl .ClassFileInfo initial = getClassFileInfo (classToInstrument );
135+ var internalClassName = Type .getInternalName (classToInstrument );
136+
137+ byte [] instrumentedBytecode = instrumenter .instrumentClass (internalClassName , initial .bytecodes ());
138+ byte [] instrumentedTwiceBytecode = instrumenter .instrumentClass (internalClassName , instrumentedBytecode );
139+
140+ logger .trace (() -> Strings .format ("Bytecode after 1st instrumentation:\n %s" , bytecode2text (instrumentedBytecode )));
141+ logger .trace (() -> Strings .format ("Bytecode after 2nd instrumentation:\n %s" , bytecode2text (instrumentedTwiceBytecode )));
142+
143+ Class <?> newClass = new TestLoader (Testable .class .getClassLoader ()).defineClassFromBytes (
144+ ClassToInstrument .class .getName () + "_NEW_NEW" ,
145+ instrumentedTwiceBytecode
146+ );
147+
148+ getTestChecks ().isActive = true ;
149+ getTestChecks ().checkSystemExitCallCount = 0 ;
150+
151+ assertThrows (TestException .class , () -> callStaticMethod (newClass , "systemExit" , 123 ));
152+ assertThat (getTestChecks ().checkSystemExitCallCount , is (1 ));
153+ }
154+
155+ public void testClassAllMethodsAreInstrumentedFirstPass () throws Exception {
156+ var classToInstrument = ClassToInstrument .class ;
157+ var instrumenter = createInstrumenter (classToInstrument , "systemExit" , "anotherSystemExit" );
158+
159+ InstrumenterImpl .ClassFileInfo initial = getClassFileInfo (classToInstrument );
160+ var internalClassName = Type .getInternalName (classToInstrument );
161+
162+ byte [] instrumentedBytecode = instrumenter .instrumentClass (internalClassName , initial .bytecodes ());
163+ byte [] instrumentedTwiceBytecode = instrumenter .instrumentClass (internalClassName , instrumentedBytecode );
164+
165+ logger .trace (() -> Strings .format ("Bytecode after 1st instrumentation:\n %s" , bytecode2text (instrumentedBytecode )));
166+ logger .trace (() -> Strings .format ("Bytecode after 2nd instrumentation:\n %s" , bytecode2text (instrumentedTwiceBytecode )));
167+
168+ Class <?> newClass = new TestLoader (Testable .class .getClassLoader ()).defineClassFromBytes (
169+ ClassToInstrument .class .getName () + "_NEW_NEW" ,
170+ instrumentedTwiceBytecode
171+ );
172+
173+ getTestChecks ().isActive = true ;
174+ getTestChecks ().checkSystemExitCallCount = 0 ;
175+
176+ assertThrows (TestException .class , () -> callStaticMethod (newClass , "systemExit" , 123 ));
177+ assertThat (getTestChecks ().checkSystemExitCallCount , is (1 ));
178+
179+ assertThrows (TestException .class , () -> callStaticMethod (newClass , "anotherSystemExit" , 123 ));
180+ assertThat (getTestChecks ().checkSystemExitCallCount , is (2 ));
181+ }
182+
183+ /** This test doesn't replace ClassToInstrument in-place but instead loads a separate
184+ * class ClassToInstrument_NEW that contains the instrumentation. Because of this,
185+ * we need to configure the Transformer to use a MethodKey and instrumentationMethod
186+ * with slightly different signatures (using the common interface Testable) which
187+ * is not what would happen when it's run by the agent.
188+ */
189+ private InstrumenterImpl createInstrumenter (Class <?> classToInstrument , String ... methodNames ) throws NoSuchMethodException {
190+ Method v1 = EntitlementChecks .class .getMethod ("checkSystemExit" , Class .class , int .class );
191+ var methods = Arrays .stream (methodNames ).map (name -> {
192+ try {
193+ return instrumentationService .methodKeyForTarget (classToInstrument .getMethod (name , int .class ));
194+ } catch (NoSuchMethodException e ) {
195+ throw new RuntimeException (e );
196+ }
197+ }).collect (Collectors .toUnmodifiableMap (name -> name , name -> v1 ));
198+
199+ return new InstrumenterImpl ("_NEW" , methods );
122200 }
123201
124202 /**
125203 * Calling a static method of a dynamically loaded class is significantly more cumbersome
126204 * than calling a virtual method.
127205 */
128- private static void callStaticSystemExit (Class <?> c , int status ) throws NoSuchMethodException , IllegalAccessException {
206+ private static void callStaticMethod (Class <?> c , String methodName , int status ) throws NoSuchMethodException , IllegalAccessException {
129207 try {
130- c .getMethod ("systemExit" , int .class ).invoke (null , status );
208+ c .getMethod (methodName , int .class ).invoke (null , status );
131209 } catch (InvocationTargetException e ) {
132210 Throwable cause = e .getCause ();
133211 if (cause instanceof TestException n ) {
0 commit comments