Cleanup: NotNull/Nullable
[idea/community.git] / java / debugger / debugger-agent / src / com / intellij / rt / debugger / agent / CaptureAgent.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.rt.debugger.agent;
3
4 import org.jetbrains.capture.org.objectweb.asm.*;
5
6 import java.io.*;
7 import java.lang.instrument.ClassFileTransformer;
8 import java.lang.instrument.Instrumentation;
9 import java.lang.instrument.UnmodifiableClassException;
10 import java.net.URI;
11 import java.security.ProtectionDomain;
12 import java.util.*;
13 import java.util.jar.JarFile;
14
15 /**
16  * @author egor
17  */
18 @SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"})
19 public class CaptureAgent {
20   public static final String AGENT_STORAGE_JAR = "debugger-agent-storage.jar";
21   private static Instrumentation ourInstrumentation;
22
23   private static final Map<String, List<InstrumentPoint>> myInstrumentPoints = new HashMap<String, List<InstrumentPoint>>();
24
25   public static void premain(String args, Instrumentation instrumentation) {
26     // never instrument twice
27     if (System.getProperty("intellij.debug.agent") != null) {
28       System.err.println("Capture agent: more than one agent is not allowed, skipping");
29       return;
30     }
31     System.setProperty("intellij.debug.agent", "true");
32
33     ourInstrumentation = instrumentation;
34     try {
35       appendStorageJar(instrumentation);
36
37       readSettings(args);
38
39       instrumentation.addTransformer(new CaptureTransformer(), true);
40
41       // Trying to reinstrument java.lang.Thread
42       // fails with dcevm, does not work with other vms :(
43       //for (Class aClass : instrumentation.getAllLoadedClasses()) {
44       //  String name = aClass.getName().replaceAll("\\.", "/");
45       //  if (myCapturePoints.containsKey(name) || myInsertPoints.containsKey(name)) {
46       //    try {
47       //      instrumentation.retransformClasses(aClass);
48       //    }
49       //    catch (UnmodifiableClassException e) {
50       //      e.printStackTrace();
51       //    }
52       //  }
53       //}
54
55       setupJboss();
56
57       if (CaptureStorage.DEBUG) {
58         System.out.println("Capture agent: ready");
59       }
60     }
61     catch (Throwable e) {
62       System.err.println("Critical error in IDEA Async Stack Traces instrumenting agent. Agent is now disabled. Please report to IDEA support:");
63       e.printStackTrace();
64     }
65   }
66
67   @SuppressWarnings("SSBasedInspection")
68   private static void appendStorageJar(Instrumentation instrumentation) throws IOException {
69     File storageJar = null;
70
71     // do not extract if storage jar is available nearby
72     try {
73       storageJar = new File(new File(CaptureAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(),
74                             AGENT_STORAGE_JAR);
75     }
76     catch (Exception e) {
77       try {
78         String path = "/" + CaptureAgent.class.getName().replace('.', '/') + ".class";
79         String classResource = CaptureAgent.class.getResource(path).getFile();
80         if (classResource.startsWith("file:")) {
81           storageJar = new File(new File(classResource.substring(5, classResource.length() - path.length() - 1)).getParentFile(),
82                                 AGENT_STORAGE_JAR);
83         }
84       } catch (Exception ignored) {
85       }
86     }
87
88     if (storageJar == null || !storageJar.exists()) {
89       InputStream inputStream = CaptureAgent.class.getResourceAsStream("/" + AGENT_STORAGE_JAR);
90       try {
91         storageJar = File.createTempFile("debugger-agent-storage", ".jar");
92         storageJar.deleteOnExit();
93         OutputStream outStream = new FileOutputStream(storageJar);
94         try {
95           byte[] buffer = new byte[10 * 1024];
96           int bytesRead;
97           while ((bytesRead = inputStream.read(buffer)) != -1) {
98             outStream.write(buffer, 0, bytesRead);
99           }
100         }
101         finally {
102           outStream.close();
103         }
104       }
105       finally {
106         inputStream.close();
107       }
108     }
109
110     instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(storageJar));
111   }
112
113   private static void setupJboss() {
114     String modulesKey = "jboss.modules.system.pkgs";
115     String property = System.getProperty(modulesKey, "");
116     if (!property.isEmpty()) {
117       property += ",";
118     }
119     property += "com.intellij.rt";
120     System.setProperty(modulesKey, property);
121   }
122
123   private static void readSettings(String uri) {
124     if (uri == null || uri.isEmpty()) {
125       return;
126     }
127
128     Properties properties = new Properties();
129     File file;
130     try {
131       FileReader reader = null;
132       try {
133         file = new File(new URI(uri));
134         reader = new FileReader(file);
135         properties.load(reader);
136       }
137       finally {
138         if (reader != null) {
139           reader.close();
140         }
141       }
142     }
143     catch (Exception e) {
144       System.out.println("Capture agent: unable to read settings");
145       e.printStackTrace();
146       return;
147     }
148
149     if (Boolean.parseBoolean(properties.getProperty("disabled", "false"))) {
150       CaptureStorage.setEnabled(false);
151     }
152
153     for (Map.Entry<Object, Object> entry : properties.entrySet()) {
154       addPoint((String)entry.getKey(), (String)entry.getValue());
155     }
156
157     // delete settings file only if it was read correctly
158     if (Boolean.parseBoolean(properties.getProperty("deleteSettings", "true"))) {
159       //noinspection ResultOfMethodCallIgnored
160       file.delete();
161     }
162   }
163
164   private static class CaptureTransformer implements ClassFileTransformer {
165     @Override
166     public byte[] transform(ClassLoader loader,
167                             String className,
168                             Class<?> classBeingRedefined,
169                             ProtectionDomain protectionDomain,
170                             byte[] classfileBuffer) {
171       if (className != null && classBeingRedefined == null) { // we do not support redefinition or retransform
172         List<InstrumentPoint> classPoints = myInstrumentPoints.get(className);
173         if (classPoints != null) {
174           try {
175             ClassReader reader = new ClassReader(classfileBuffer);
176             ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
177
178             reader.accept(new CaptureInstrumentor(Opcodes.API_VERSION, writer, classPoints), 0);
179             byte[] bytes = writer.toByteArray();
180
181             if (CaptureStorage.DEBUG) {
182               try {
183                 FileOutputStream stream = new FileOutputStream("instrumented_" + className.replaceAll("/", "_") + ".class");
184                 try {
185                   stream.write(bytes);
186                 }
187                 finally {
188                   stream.close();
189                 }
190               }
191               catch (IOException e) {
192                 e.printStackTrace();
193               }
194             }
195
196             return bytes;
197           }
198           catch (Exception e) {
199             System.out.println("Capture agent: failed to instrument " + className);
200             e.printStackTrace();
201           }
202         }
203       }
204       return null;
205     }
206   }
207
208   private static class CaptureInstrumentor extends ClassVisitor {
209     private final List<InstrumentPoint> myInstrumentPoints;
210     private final Map<String, String> myFields = new HashMap<String, String>();
211     private String mySuperName;
212
213     CaptureInstrumentor(int api, ClassVisitor cv, List<InstrumentPoint> instrumentPoints) {
214       super(api, cv);
215       this.myInstrumentPoints = instrumentPoints;
216     }
217
218     private static String getNewName(String name) {
219       return name + CaptureStorage.GENERATED_INSERT_METHOD_POSTFIX;
220     }
221
222     private static String getMethodDisplayName(String className, String methodName, String desc) {
223       return className + "." + methodName + desc;
224     }
225
226     @Override
227     public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
228       mySuperName = superName;
229       super.visit(version, access, name, signature, superName, interfaces);
230     }
231
232     @Override
233     public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
234       myFields.put(name, desc);
235       return super.visitField(access, name, desc, signature, value);
236     }
237
238     @Override
239     public MethodVisitor visitMethod(final int access, String name, final String desc, String signature, String[] exceptions) {
240       if ((access & Opcodes.ACC_BRIDGE) == 0) {
241         for (final InstrumentPoint point : myInstrumentPoints) {
242           if (point.matchesMethod(name, desc)) {
243             final String methodDisplayName = getMethodDisplayName(point.myClassName, name, desc);
244             if (CaptureStorage.DEBUG) {
245               System.out.println(
246                 "Capture agent: instrumented " + (point.myCapture ? "capture" : "insert") + " point at " + methodDisplayName);
247             }
248             if (point.myCapture) { // capture
249               // for constructors and "this" key - move capture to after the super constructor call
250               if (CONSTRUCTOR.equals(name) && point.myKeyProvider == THIS_KEY_PROVIDER) {
251                 return new MethodVisitor(api, super.visitMethod(access, name, desc, signature, exceptions)) {
252                   boolean captured = false;
253
254                   @Override
255                   public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
256                     super.visitMethodInsn(opcode, owner, name, desc, itf);
257                     if (opcode == Opcodes.INVOKESPECIAL &&
258                         !captured &&
259                         owner.equals(mySuperName) &&
260                         name.equals(CONSTRUCTOR)) { // super constructor
261                       capture(mv, point.myKeyProvider, (access & Opcodes.ACC_STATIC) != 0,
262                               Type.getMethodType(desc).getArgumentTypes(), methodDisplayName);
263                       captured = true;
264                     }
265                   }
266                 };
267               }
268               else {
269                 return new MethodVisitor(api, super.visitMethod(access, name, desc, signature, exceptions)) {
270                   @Override
271                   public void visitCode() {
272                     capture(mv, point.myKeyProvider, (access & Opcodes.ACC_STATIC) != 0, Type.getMethodType(desc).getArgumentTypes(),
273                             methodDisplayName);
274                     super.visitCode();
275                   }
276                 };
277               }
278             }
279             else { // insert
280               generateWrapper(access, name, desc, signature, exceptions, point, methodDisplayName);
281               return super.visitMethod(access, getNewName(name), desc, signature, exceptions);
282             }
283           }
284         }
285       }
286       return super.visitMethod(access, name, desc, signature, exceptions);
287     }
288
289     private void generateWrapper(int access,
290                                  String name,
291                                  String desc,
292                                  String signature,
293                                  String[] exceptions,
294                                  InstrumentPoint insertPoint,
295                                  String methodDisplayName) {
296       MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
297
298       Label start = new Label();
299       mv.visitLabel(start);
300
301       boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
302       Type[] argumentTypes = Type.getMethodType(desc).getArgumentTypes();
303
304       insertEnter(mv, insertPoint.myKeyProvider, isStatic, argumentTypes, methodDisplayName);
305
306       // this
307       mv.visitVarInsn(Opcodes.ALOAD, 0);
308       // params
309       int index = isStatic ? 0 : 1;
310       for (Type t : argumentTypes) {
311         mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), index);
312         index += t.getSize();
313       }
314       // original call
315       mv.visitMethodInsn(isStatic ? Opcodes.INVOKESTATIC : Opcodes.INVOKESPECIAL,
316                          insertPoint.myClassName, getNewName(insertPoint.myMethodName), desc, false);
317
318       Label end = new Label();
319       mv.visitLabel(end);
320
321       // regular exit
322       insertExit(mv, insertPoint.myKeyProvider, isStatic, argumentTypes, methodDisplayName);
323       mv.visitInsn(Type.getReturnType(desc).getOpcode(Opcodes.IRETURN));
324
325       Label catchLabel = new Label();
326       mv.visitLabel(catchLabel);
327       mv.visitTryCatchBlock(start, end, catchLabel, null);
328
329       // exception exit
330       insertExit(mv, insertPoint.myKeyProvider, isStatic, argumentTypes, methodDisplayName);
331       mv.visitInsn(Opcodes.ATHROW);
332
333       mv.visitMaxs(0, 0);
334       mv.visitEnd();
335     }
336
337     private void capture(MethodVisitor mv,
338                          KeyProvider keyProvider,
339                          boolean isStatic,
340                          Type[] argumentTypes,
341                          String methodDisplayName) {
342       storageCall(mv, keyProvider, isStatic, argumentTypes, "capture", methodDisplayName);
343     }
344
345     private void insertEnter(MethodVisitor mv,
346                              KeyProvider keyProvider,
347                              boolean isStatic,
348                              Type[] argumentTypes,
349                              String methodDisplayName) {
350       storageCall(mv, keyProvider, isStatic, argumentTypes, "insertEnter", methodDisplayName);
351     }
352
353     private void insertExit(MethodVisitor mv,
354                             KeyProvider keyProvider,
355                             boolean isStatic,
356                             Type[] argumentTypes,
357                             String methodDisplayName) {
358       storageCall(mv, keyProvider, isStatic, argumentTypes, "insertExit", methodDisplayName);
359     }
360
361     private void storageCall(MethodVisitor mv,
362                              KeyProvider keyProvider,
363                              boolean isStatic,
364                              Type[] argumentTypes,
365                              String storageMethodName,
366                              String methodDisplayName) {
367       keyProvider.loadKey(mv, isStatic, argumentTypes, methodDisplayName, this);
368       mv.visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), storageMethodName,
369                          "(Ljava/lang/Object;)V", false);
370     }
371   }
372
373   private static class InstrumentPoint {
374     final static String ANY_DESC = "*";
375
376     final boolean myCapture;
377     final String myClassName;
378     final String myMethodName;
379     final String myMethodDesc;
380     final KeyProvider myKeyProvider;
381
382     InstrumentPoint(boolean capture, String className, String methodName, String methodDesc, KeyProvider keyProvider) {
383       myCapture = capture;
384       myClassName = className;
385       myMethodName = methodName;
386       myMethodDesc = methodDesc;
387       myKeyProvider = keyProvider;
388     }
389
390     boolean matchesMethod(String name, String desc) {
391       if (!myMethodName.equals(name)) {
392         return false;
393       }
394       return myMethodDesc.equals(ANY_DESC) || myMethodDesc.equals(desc);
395     }
396   }
397
398   // to be run from the debugger
399   @SuppressWarnings("unused")
400   public static void addCapturePoints(String capturePoints) throws UnmodifiableClassException, IOException {
401     if (CaptureStorage.DEBUG) {
402       System.out.println("Capture agent: adding points " + capturePoints);
403     }
404
405     Properties properties = new Properties();
406     properties.load(new StringReader(capturePoints));
407
408     Set<String> classNames = new HashSet<String>();
409
410     for (Map.Entry<Object, Object> entry : properties.entrySet()) {
411       InstrumentPoint point = addPoint((String)entry.getKey(), (String)entry.getValue());
412       if (point != null) {
413         classNames.add(point.myClassName.replaceAll("/", "\\."));
414       }
415     }
416
417     List<Class> classes = new ArrayList<Class>(classNames.size());
418     for (Class aClass : ourInstrumentation.getAllLoadedClasses()) {
419       if (classNames.contains(aClass.getName())) {
420         classes.add(aClass);
421       }
422     }
423
424     if (!classes.isEmpty()) {
425       if (CaptureStorage.DEBUG) {
426         System.out.println("Capture agent: retransforming " + classes);
427       }
428
429       //noinspection SSBasedInspection
430       ourInstrumentation.retransformClasses(classes.toArray(new Class[0]));
431     }
432   }
433
434   private static InstrumentPoint addPoint(String propertyKey, String propertyValue) {
435     if (propertyKey.startsWith("capture")) {
436       return addPoint(true, propertyValue);
437     }
438     else if (propertyKey.startsWith("insert")) {
439       return addPoint(false, propertyValue);
440     }
441     return null;
442   }
443
444   private static InstrumentPoint addPoint(boolean capture, String line) {
445     String[] split = line.split(" ");
446     KeyProvider keyProvider = createKeyProvider(Arrays.copyOfRange(split, 3, split.length));
447     return addCapturePoint(capture, split[0], split[1], split[2], keyProvider);
448   }
449
450   private static InstrumentPoint addCapturePoint(boolean capture,
451                                                  String className,
452                                                  String methodName,
453                                                  String methodDesc,
454                                                  KeyProvider keyProvider) {
455     List<InstrumentPoint> points = myInstrumentPoints.get(className);
456     if (points == null) {
457       points = new ArrayList<InstrumentPoint>(1);
458       myInstrumentPoints.put(className, points);
459     }
460     InstrumentPoint point = new InstrumentPoint(capture, className, methodName, methodDesc, keyProvider);
461     points.add(point);
462     return point;
463   }
464
465   private static final KeyProvider FIRST_PARAM = param(0);
466
467   static final KeyProvider THIS_KEY_PROVIDER = new KeyProvider() {
468     @Override
469     public void loadKey(MethodVisitor mv,
470                         boolean isStatic,
471                         Type[] argumentTypes,
472                         String methodDisplayName,
473                         CaptureInstrumentor instrumentor) {
474       if (isStatic) {
475         throw new IllegalStateException("This is not available in a static method " + methodDisplayName);
476       }
477       mv.visitVarInsn(Opcodes.ALOAD, 0);
478     }
479   };
480
481   private static KeyProvider createKeyProvider(String[] line) {
482     if ("this".equals(line[0])) {
483       return THIS_KEY_PROVIDER;
484     }
485     if (isNumber(line[0])) {
486       try {
487         return new ParamKeyProvider(Integer.parseInt(line[0]));
488       }
489       catch (NumberFormatException ignored) {
490       }
491     }
492     return new FieldKeyProvider(line[0], line[1]);
493   }
494
495   private static boolean isNumber(String s) {
496     if (s == null) return false;
497     for (int i = 0; i < s.length(); ++i) {
498       if (!Character.isDigit(s.charAt(i))) return false;
499     }
500     return true;
501   }
502
503   private interface KeyProvider {
504     void loadKey(MethodVisitor mv, boolean isStatic, Type[] argumentTypes, String methodDisplayName, CaptureInstrumentor instrumentor);
505   }
506
507   private static class FieldKeyProvider implements KeyProvider {
508     private final String myClassName;
509     private final String myFieldName;
510
511     FieldKeyProvider(String className, String fieldName) {
512       myClassName = className;
513       myFieldName = fieldName;
514     }
515
516     @Override
517     public void loadKey(MethodVisitor mv,
518                         boolean isStatic,
519                         Type[] argumentTypes,
520                         String methodDisplayName,
521                         CaptureInstrumentor instrumentor) {
522       String desc = instrumentor.myFields.get(myFieldName);
523       if (desc == null) {
524         throw new IllegalStateException("Field " + myFieldName + " was not found");
525       }
526       mv.visitVarInsn(Opcodes.ALOAD, 0);
527       mv.visitFieldInsn(Opcodes.GETFIELD, myClassName, myFieldName, desc);
528     }
529   }
530
531   private static class ParamKeyProvider implements KeyProvider {
532     private final int myIdx;
533
534     ParamKeyProvider(int idx) {
535       myIdx = idx;
536     }
537
538     @Override
539     public void loadKey(MethodVisitor mv,
540                         boolean isStatic,
541                         Type[] argumentTypes,
542                         String methodDisplayName,
543                         CaptureInstrumentor instrumentor) {
544       int index = isStatic ? 0 : 1;
545       if (myIdx >= argumentTypes.length) {
546         throw new IllegalStateException(
547           "Argument with id " + myIdx + " is not available, method " + methodDisplayName + " has only " + argumentTypes.length);
548       }
549       int sort = argumentTypes[myIdx].getSort();
550       if (sort != Type.OBJECT && sort != Type.ARRAY) {
551         throw new IllegalStateException(
552           "Argument with id " + myIdx + " in method " + methodDisplayName + " must be an object");
553       }
554       for (int i = 0; i < myIdx; i++) {
555         index += argumentTypes[i].getSize();
556       }
557       mv.visitVarInsn(Opcodes.ALOAD, index);
558     }
559   }
560
561   private static void addCapture(String className, String methodName, KeyProvider key) {
562     addCapturePoint(true, className, methodName, InstrumentPoint.ANY_DESC, key);
563   }
564
565   private static void addInsert(String className, String methodName, KeyProvider key) {
566     addCapturePoint(false, className, methodName, InstrumentPoint.ANY_DESC, key);
567   }
568
569   private static KeyProvider param(int idx) {
570     return new ParamKeyProvider(idx);
571   }
572
573   public static final String CONSTRUCTOR = "<init>";
574
575   // predefined points
576   static {
577     addCapture("java/awt/event/InvocationEvent", CONSTRUCTOR, THIS_KEY_PROVIDER);
578     addInsert("java/awt/event/InvocationEvent", "dispatch", THIS_KEY_PROVIDER);
579
580     addCapture("java/lang/Thread", "start", THIS_KEY_PROVIDER);
581     addInsert("java/lang/Thread", "run", THIS_KEY_PROVIDER);
582
583     addCapture("java/util/concurrent/FutureTask", CONSTRUCTOR, THIS_KEY_PROVIDER);
584     addInsert("java/util/concurrent/FutureTask", "run", THIS_KEY_PROVIDER);
585     addInsert("java/util/concurrent/FutureTask", "runAndReset", THIS_KEY_PROVIDER);
586
587     addCapture("java/util/concurrent/CompletableFuture$AsyncSupply", CONSTRUCTOR, THIS_KEY_PROVIDER);
588     addInsert("java/util/concurrent/CompletableFuture$AsyncSupply", "run", THIS_KEY_PROVIDER);
589
590     addCapture("java/util/concurrent/CompletableFuture$AsyncRun", CONSTRUCTOR, THIS_KEY_PROVIDER);
591     addInsert("java/util/concurrent/CompletableFuture$AsyncRun", "run", THIS_KEY_PROVIDER);
592
593     addCapture("java/util/concurrent/CompletableFuture$UniAccept", CONSTRUCTOR, THIS_KEY_PROVIDER);
594     addInsert("java/util/concurrent/CompletableFuture$UniAccept", "tryFire", THIS_KEY_PROVIDER);
595
596     addCapture("java/util/concurrent/CompletableFuture$UniRun", CONSTRUCTOR, THIS_KEY_PROVIDER);
597     addInsert("java/util/concurrent/CompletableFuture$UniRun", "tryFire", THIS_KEY_PROVIDER);
598
599     // netty
600     addCapture("io/netty/util/concurrent/SingleThreadEventExecutor", "addTask", FIRST_PARAM);
601     addInsert("io/netty/util/concurrent/AbstractEventExecutor", "safeExecute", FIRST_PARAM);
602
603     // scala
604     addCapture("scala/concurrent/impl/Future$PromiseCompletingRunnable", CONSTRUCTOR, THIS_KEY_PROVIDER);
605     addInsert("scala/concurrent/impl/Future$PromiseCompletingRunnable", "run", THIS_KEY_PROVIDER);
606
607     addCapture("scala/concurrent/impl/CallbackRunnable", CONSTRUCTOR, THIS_KEY_PROVIDER);
608     addInsert("scala/concurrent/impl/CallbackRunnable", "run", THIS_KEY_PROVIDER);
609
610     // akka-scala
611     addCapture("akka/actor/ScalaActorRef", "$bang", FIRST_PARAM);
612     addCapture("akka/actor/RepointableActorRef", "$bang", FIRST_PARAM);
613     addCapture("akka/actor/LocalActorRef", "$bang", FIRST_PARAM);
614     addInsert("akka/actor/Actor$class", "aroundReceive", param(2));
615
616     // JavaFX
617     addCapture("com/sun/glass/ui/InvokeLaterDispatcher", "invokeLater", FIRST_PARAM);
618     addInsert("com/sun/glass/ui/InvokeLaterDispatcher$Future", "run",
619               new FieldKeyProvider("com/sun/glass/ui/InvokeLaterDispatcher$Future", "runnable"));
620   }
621 }