blob: 0f292878e1e4904de274806f663ea16bb8f129a7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2020 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.core.tests.eval.target;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
/**
* A code snippet runner loads code snippet classes and global
* variable classes, and that run the code snippet classes.
* <p>
* When started, this runner first connects using TCP/IP to the provided port number.
* If a regular classpath directory is provided, it writes the class definitions it gets from the IDE
* to this directory (or to the bootclasspath directory if the class name starts with "java") and it
* lets the system class loader (or the bootstrap class loader if it is a "java" class) load
* the class.
* If the regular classpath directory is null, it uses a code snippet class loader to load the classes
* it gets from the IDE.
* <p>
* IMPORTANT NOTE:
* Using a code snippet class loader has the following limitation when the code snippet is ran:
* <ul>
* <li>The code snippet class can access only public classes, and public members or these classes.
* This is because the "runtime package" of the code snippet class is always different from
* the "runtime package" of the class it is trying to access since the class loaders are
* different.
* <li>The code snippet class cannot be defined in a "java.*" package. Only the bootstrap class
* loader can load such a class.
* </ul>
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class CodeSnippetRunner {
public static CodeSnippetRunner theRunner;
static final String CODE_SNIPPET_CLASS_NAME = "org.eclipse.jdt.internal.eval.target.CodeSnippet";
static final String RUN_METHOD_NAME = "run";
static final String GET_RESULT_TYPE_METHOD_NAME = "getResultType";
static final String GET_RESULT_VALUE_METHOD_NAME = "getResultValue";
IDEInterface ide;
String classPathDirectory;
String bootclassPathDirectory;
CodeSnippetClassLoader loader;
Class codeSnippetClass = null;
/**
* Creates a new code snippet runner.
*/
public CodeSnippetRunner(int portNumber, String classPathDirectory, String bootclassPathDirectory) {
this.ide = new IDEInterface(portNumber);
if (classPathDirectory != null) {
this.classPathDirectory = classPathDirectory;
if (bootclassPathDirectory != null) {
this.bootclassPathDirectory = bootclassPathDirectory;
}
} else {
this.loader = new CodeSnippetClassLoader();
}
}
/**
* Returns the forward slash separated class name from the given class definition.
*/
private String className(byte[] classDefinition) {
// NB: The following code was copied from org.eclipse.jdt.internal.compiler.cfmt,
// thus it is highly dependent on the class file format.
int readOffset = 10;
try {
int constantPoolCount = u2At(8, classDefinition);
int[] constantPoolOffsets = new int[constantPoolCount];
for (int i = 1; i < constantPoolCount; i++) {
int tag = u1At(readOffset, classDefinition);
switch (tag) {
case 1 : // Utf8Tag
constantPoolOffsets[i] = readOffset;
readOffset += u2At(readOffset + 1, classDefinition);
readOffset += 3; // ConstantUtf8.fixedSize
break;
case 3 : // IntegerTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantInteger.fixedSize
break;
case 4 : // FloatTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantFloat.fixedSize
break;
case 5 : // LongTag
constantPoolOffsets[i] = readOffset;
readOffset += 9; // ConstantLong.fixedSize
i++;
break;
case 6 : // DoubleTag
constantPoolOffsets[i] = readOffset;
readOffset += 9; // ConstantDouble.fixedSize
i++;
break;
case 7 : // ClassTag
constantPoolOffsets[i] = readOffset;
readOffset += 3; // ConstantClass.fixedSize
break;
case 8 : // StringTag
constantPoolOffsets[i] = readOffset;
readOffset += 3; // ConstantString.fixedSize
break;
case 9 : // FieldRefTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantFieldRef.fixedSize
break;
case 10 : // MethodRefTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantMethodRef.fixedSize
break;
case 11 : // InterfaceMethodRefTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantInterfaceMethodRef.fixedSize
break;
case 12 : // NameAndTypeTag
constantPoolOffsets[i] = readOffset;
readOffset += 5; // ConstantNameAndType.fixedSize
}
}
// Skip access flags
readOffset += 2;
// Read the classname, use exception handlers to catch bad format
int constantPoolIndex = u2At(readOffset, classDefinition);
int utf8Offset = constantPoolOffsets[u2At(constantPoolOffsets[constantPoolIndex] + 1, classDefinition)];
char[] className = utf8At(utf8Offset + 3, u2At(utf8Offset + 1, classDefinition), classDefinition);
return new String(className);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
return null;
}
}
/**
* Creates a new instance of the given class. It is
* assumed that it is a subclass of CodeSnippet.
*/
Object createCodeSnippet(Class snippetClass) {
Object object = null;
try {
object = snippetClass.getDeclaredConstructor().newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
} catch (IllegalAccessException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
} catch (IllegalArgumentException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
} catch (InvocationTargetException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
} catch (NoSuchMethodException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
} catch (SecurityException e) {
e.printStackTrace();
this.ide.sendResult(void.class, null);
return null;
}
return object;
}
/**
* Whether this code snippet runner is currently running.
*/
public boolean isRunning() {
return this.ide.isConnected();
}
/**
* Starts a new CodeSnippetRunner that will serve code snippets from the IDE.
* It waits for a connection on the given evaluation port number.
* <p>
* Usage: java org.eclipse.jdt.tests.eval.target.CodeSnippetRunner -evalport <portNumber> [-options] [<mainClassName>] [<arguments>]
* where options include:
* -cscp <codeSnippetClasspath> the the classpath directory for the code snippet classes.
* that are not defined in a "java.*" package.
* -csbp <codeSnippetBootClasspath> the bootclasspath directory for the code snippet classes
* that are defined in a "java.*" package.
* <p>
* The mainClassName and its arguments are optional: when not present only the server will start
* and run until the VM is shut down, when present the server will start, the main class will run
* but the server will exit when the main class has finished running.
*/
public static void main(String[] args) {
int length = args.length;
if (length < 2 || !args[0].toLowerCase().equals("-evalport")) {
printUsage();
return;
}
int evalPort = Integer.parseInt(args[1]);
String classPath = null;
String bootPath = null;
int mainClass = -1;
for (int i = 2; i < length; i++) {
String arg = args[i];
if (arg.startsWith("-")) {
if (arg.toLowerCase().equals("-cscp")) {
if (++i < length) {
classPath = args[i];
} else {
printUsage();
return;
}
} else if (arg.toLowerCase().equals("-csbp")) {
if (++i < length) {
bootPath = args[i];
} else {
printUsage();
return;
}
}
} else {
mainClass = i;
break;
}
}
theRunner = new CodeSnippetRunner(evalPort, classPath, bootPath);
if (mainClass == -1) {
theRunner.start();
} else {
Thread server = new Thread() {
@Override
public void run() {
theRunner.start();
}
};
server.setDaemon(true);
server.start();
int mainArgsLength = length-mainClass-1;
String[] mainArgs = new String[mainArgsLength];
System.arraycopy(args, mainClass+1, mainArgs, 0, mainArgsLength);
try {
Class clazz = Class.forName(args[mainClass]);
Method mainMethod = clazz.getMethod("main", new Class[] {String[].class});
mainMethod.invoke(null, (Object[]) mainArgs);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
private static void printUsage() {
System.out.println("Usage: java org.eclipse.jdt.tests.eval.target.CodeSnippetRunner -evalport <portNumber> [-options] [<mainClassName>] [<arguments>]");
System.out.println("where options include:");
System.out.println("-cscp <codeSnippetClasspath> the the classpath directory for the code snippet classes.");
System.out.println("that are not defined in a \"java.*\" package.");
System.out.println("-csbp <codeSnippetBootClasspath> the bootclasspath directory for the code snippet classes");
System.out.println("that are defined in a \"java.*\" package.");
}
/**
* Loads the given class definitions. The way these class definitions are loaded is described
* in the CodeSnippetRunner constructor.
* The class definitions are code snippet classes and/or global variable classes.
* Code snippet classes are assumed be direct or indirect subclasses of CodeSnippet and implement
* only the run()V method.
* They are instanciated and run.
* Global variable classes are assumed to be direct subclasses of CodeSnippet. Their fields are assumed
* to be static. The value of each field is sent back to the IDE.
*/
void processClasses(boolean mustRun, byte[][] classDefinitions) {
// store the class definitions (either in the code snippet class loader or on disk)
String[] newClasses = new String[classDefinitions.length];
for (int i = 0; i < classDefinitions.length; i++) {
byte[] classDefinition = classDefinitions[i];
String classFileName = className(classDefinition);
String className = classFileName.replace('/', '.');
if (this.loader != null) {
this.loader.storeClassDefinition(className, classDefinition);
} else {
writeClassOnDisk(classFileName, classDefinition);
}
newClasses[i] = className;
}
// load the classes and collect code snippet classes
List<Class> codeSnippetClasses = new ArrayList<>();
for (int i = 0; i < newClasses.length; i++) {
String className = newClasses[i];
Class clazz = null;
if (this.loader != null) {
clazz = this.loader.loadIfNeeded(className);
if (clazz == null) {
System.err.println("Could not find class definition for " + className);
break;
}
} else {
// use the system class loader
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace(); // should never happen since we just wrote it on disk
this.ide.sendResult(void.class, null);
break;
}
}
Class superclass = clazz.getSuperclass();
Method[] methods = clazz.getDeclaredMethods();
if (this.codeSnippetClass == null) {
if (superclass.equals(Object.class) && clazz.getName().equals(CODE_SNIPPET_CLASS_NAME)) {
// The CodeSnippet class is being deployed
this.codeSnippetClass = clazz;
} else {
System.out.println("Expecting CodeSnippet class to be deployed first");
}
} else if (superclass.equals(this.codeSnippetClass)) {
// It may be a code snippet class with no global variable
if (methods.length == 1 && methods[0].getName().equals(RUN_METHOD_NAME)) {
codeSnippetClasses.add(clazz);
}
// Evaluate global variables and send result back
Field[] fields = clazz.getDeclaredFields();
for (int j = 0; j < fields.length; j++) {
Field field = fields[j];
if (Modifier.isPublic(field.getModifiers())) {
try {
this.ide.sendResult(field.getType(), field.get(null));
} catch (IllegalAccessException e) {
e.printStackTrace(); // Cannot happen because the field is public
this.ide.sendResult(void.class, null);
break;
}
}
}
} else if (this.codeSnippetClass.equals(superclass.getSuperclass()) && methods.length == 1 && methods[0].getName().equals("run")) {
// It is a code snippet class with a global variable superclass
codeSnippetClasses.add(clazz);
}
}
// run the code snippet classes
if (codeSnippetClasses.size() != 0 && mustRun) {
for (Class class1 : codeSnippetClasses) {
Object codeSnippet = createCodeSnippet(class1);
if (codeSnippet != null) {
runCodeSnippet(codeSnippet);
}
}
}
}
/**
* Runs the given code snippet in a new thread and send the result back to the IDE.
*/
void runCodeSnippet(final Object snippet) {
Thread thread = new Thread() {
@Override
public void run() {
try {
try {
Method runMethod = CodeSnippetRunner.this.codeSnippetClass.getMethod(RUN_METHOD_NAME, new Class[] {});
runMethod.invoke(snippet, new Object[] {});
} finally {
Method getResultTypeMethod = CodeSnippetRunner.this.codeSnippetClass.getMethod(GET_RESULT_TYPE_METHOD_NAME, new Class[] {});
Class resultType = (Class)getResultTypeMethod.invoke(snippet, new Object[] {});
Method getResultValueMethod = CodeSnippetRunner.this.codeSnippetClass.getMethod(GET_RESULT_VALUE_METHOD_NAME, new Class[] {});
Object resultValue = getResultValueMethod.invoke(snippet, new Object[] {});
CodeSnippetRunner.this.ide.sendResult(resultType, resultValue);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
System.out.println("codeSnippetClass = " + CodeSnippetRunner.this.codeSnippetClass.getName());
System.out.println("snippet.class = " + snippet.getClass().getName());
Class superclass = snippet.getClass().getSuperclass();
System.out.println("snippet.superclass = " + (superclass == null ? "null" : superclass.getName()));
e.printStackTrace();
} catch (InvocationTargetException e) {
e.getTargetException().printStackTrace();
}
}
};
thread.setDaemon(true);
thread.start();
}
/**
* Starts this code snippet runner in a different thread.
*/
public void start() {
Thread thread = new Thread("Code snippet runner") {
@Override
public void run() {
try {
CodeSnippetRunner.this.ide.connect();
} catch (IOException e) {
e.printStackTrace();
}
while (CodeSnippetRunner.this.ide.isConnected()) {
try {
processClasses(CodeSnippetRunner.this.ide.getRunFlag(), CodeSnippetRunner.this.ide.getNextClasses());
} catch (Error e) {
CodeSnippetRunner.this.ide.sendResult(void.class, null);
e.printStackTrace();
} catch (RuntimeException e) {
CodeSnippetRunner.this.ide.sendResult(void.class, null);
e.printStackTrace();
}
}
}
};
thread.start();
}
/**
* Stops this code snippet runner.
*/
public void stop() {
this.ide.disconnect();
}
private int u1At(int position, byte[] bytes) {
return bytes[position] & 0xFF;
}
private int u2At(int position, byte[] bytes) {
return ((bytes[position++] & 0xFF) << 8) + (bytes[position] & 0xFF);
}
private char[] utf8At(int readOffset, int bytesAvailable, byte[] bytes) {
int x, y, z;
int length = bytesAvailable;
char outputBuf[] = new char[bytesAvailable];
int outputPos = 0;
while (length != 0) {
x = bytes[readOffset++] & 0xFF;
length--;
if ((0x80 & x) != 0) {
y = bytes[readOffset++] & 0xFF;
length--;
if ((x & 0x20) != 0) {
z = bytes[readOffset++] & 0xFF;
length--;
x = ((x & 0x1F) << 12) + ((y & 0x3F) << 6) + (z & 0x3F);
} else {
x = ((x & 0x1F) << 6) + (y & 0x3F);
}
}
outputBuf[outputPos++] = (char) x;
}
if (outputPos != bytesAvailable) {
System.arraycopy(outputBuf, 0, (outputBuf = new char[outputPos]), 0, outputPos);
}
return outputBuf;
}
/**
* Writes the given class definition on disk. The give name is the forward slash separated
* fully qualified name of the class.
*/
private void writeClassOnDisk(String className, byte[] classDefinition) {
try {
String fileName = className.replace('/', File.separatorChar) + ".class";
File classFile = new File(
(this.bootclassPathDirectory != null &&
(className.startsWith("java") || className.replace('/', '.').equals(CODE_SNIPPET_CLASS_NAME))) ?
this.bootclassPathDirectory :
this.classPathDirectory, fileName);
File parent = new File(classFile.getParent());
parent.mkdirs();
if (!parent.exists()) {
throw new IOException("Could not create directory " + parent.getPath());
}
FileOutputStream out = null;
try {
out = new FileOutputStream(classFile);
out.write(classDefinition);
} finally {
if (out != null) {
out.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}