blob: d618be80083afba3f13b55767c0359e0da2962b9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.lib.jshell.agent;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import jdk.jshell.execution.RemoteExecutionControl;
import static jdk.jshell.execution.Util.forwardExecutionControlAndIO;
/**
*
* @author sdedic
*/
public class AgentWorker extends RemoteExecutionControl implements Executor, Runnable {
private static final Logger LOG = Logger.getLogger("org.netbeans.lib.jshell.agent.AgentWorker"); // NOI18N
public static final String PROPERTY_EXECUTOR = "org.netbeans.lib.jshell.agent.AgentWorker.executor"; // NOI18N
/**
* Reference set by instrumented classes
*/
public static volatile ClassLoader referenceClassLoader;
/**
* The JShell agent main with options read from commandline
*/
private final NbJShellAgent agent;
/**
* The control socket
*/
private final Socket socket;
private final int socketPort;
private RemoteClassLoader loader;
/**
* The Classloader last obtained from a field or method
*/
private ClassLoader lastClassLoader;
/**
* Provider for the classloader. Contacted before every user invocation
* and before every class load.
*/
private Callable<ClassLoader> loaderProvider;
/**
* Executor for user's code. Can be provided from the environment using System.properties,
* see {@link #PROPERTY_EXECUTOR}.
*/
private Executor userExecutor;
/**
* Threads which execute user code, keyed by agent's socket local port.
*/
// @GuardedBy(self)
private static final Map<Integer, Thread> userCodeExecutingThreads = new HashMap<Integer, Thread>();
private AgentWorker() {
agent = null;
socket = null;
socketPort = -1;
this.userExecutor = findExecutor();
loaderProvider = new Callable<ClassLoader>() {
public ClassLoader call() {
return loader;
}
};
setup();
}
public AgentWorker(NbJShellAgent agent, Socket controlSocket) {
this.agent = agent;
this.socket = controlSocket;
this.socketPort = controlSocket.getLocalPort();
setup();
}
private void setup() {
this.loader = new NbRemoteLoader(ClassLoader.getSystemClassLoader(), null, new URL[0]);
if (agent != null) {
if (agent.getField() != null || agent.getMethod() != null) {
loaderProvider = new LoaderEvaluator();
} else if (agent.getClassName() != null) {
loaderProvider = new LoaderAccessor(loader);
}
}
if (loaderProvider == null) {
loaderProvider = new Callable<ClassLoader>() {
public ClassLoader call() {
return loader;
}
};
}
this.userExecutor = findExecutor();
}
private Executor findExecutor() {
Object o = System.getProperties().get(PROPERTY_EXECUTOR);;
if (o instanceof Executor) {
return this.userExecutor = (Executor)o;
} else if (o instanceof String) {
try {
Class executorClazz = Class.forName((String)o);
return (Executor)executorClazz.newInstance();
} catch (ClassNotFoundException ex) {
LOG.log(Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
LOG.log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
LOG.log(Level.SEVERE, null, ex);
}
}
return this;
}
private static class LoaderAccessor implements Callable<ClassLoader> {
private final ClassLoader defaultLoader;
public LoaderAccessor(ClassLoader defaultLoader) {
this.defaultLoader = defaultLoader;
}
@Override
public ClassLoader call() throws Exception {
if (referenceClassLoader != null) {
return referenceClassLoader;
} else {
return defaultLoader;
}
}
}
private class LoaderEvaluator implements Callable<ClassLoader> {
private Class clazz;
private Method method;
private Field field;
public ClassLoader call() throws Exception {
if (clazz == null) {
try {
clazz = Class.forName(agent.getClassName(), false, loader);
} catch (ClassNotFoundException ex) {
// the class may not be loaded yet, use the default loader now
return loader;
}
String m = agent.getMethod();
String f = agent.getField();
if (m != null) {
method = clazz.getDeclaredMethod(m);
if (!method.getReturnType().isAssignableFrom(ClassLoader.class) ||
(method.getModifiers() & Modifier.STATIC) == 0) {
throw new IllegalStateException("Loader access method must be static and return ClassLoader");
}
method.setAccessible(true);
} else if (f != null) {
field = clazz.getDeclaredField(f);
field.setAccessible(true);
if (!field.getType().isAssignableFrom(ClassLoader.class) ||
(field.getModifiers() & Modifier.STATIC) == 0) {
throw new IllegalStateException("Loader access field must be static and assignable to ClassLoader");
}
}
}
if (method != null) {
return (ClassLoader)method.invoke(null);
} else if (field != null) {
return (ClassLoader)field.get(null);
} else {
return loader;
}
}
}
private void installNewClassLoader(ClassLoader delegate) {
lastClassLoader = delegate;
loader = new NbRemoteLoader(delegate, loader,
additionalClasspath.toArray(new URL[additionalClasspath.size()]));
}
@Override
public void execute(Runnable command) {
command.run();
}
protected NbRemoteLoader prepareClassLoader() {
try {
ClassLoader current = loaderProvider.call();
if (current != lastClassLoader && current != loader) {
installNewClassLoader(current);
}
} catch (Exception ex) {
// don't touch
}
return (NbRemoteLoader)loader;
}
public static void main(String[] args) throws Exception {
String loopBack = null;
LOG.log(Level.INFO, "Running main, port = ", args[0]);
Socket socket = new Socket(loopBack, Integer.parseInt(args[0]));
try {
AgentWorker worker = new AgentWorker();
LOG.log(Level.INFO, "Worker created", args[0]);
InputStream inStream = socket.getInputStream();
OutputStream outStream = socket.getOutputStream();
Map<String, Consumer<OutputStream>> chans = new HashMap<>();
chans.put("out", st -> System.setOut(new PrintStream(st, true)));
chans.put("err", st -> System.setErr(new PrintStream(st, true)));
forwardExecutionControlAndIO(worker, inStream, outStream, chans, Collections.emptyMap());
} catch (EOFException ex) {
// ignore, forcible close by the tool
}
}
@Override
public Object extensionCommand(String command, Object arg) throws RunException, EngineTerminationException, InternalException {
try {
switch (command) {
case "nb_vmInfo":
return returnVMInfo(null);
case "nb_stop":
if (!(arg instanceof Integer)) {
throw new InternalError("Unexpected agent ID: " + arg);
}
performStop((Integer)arg);
return 1;
default:
throw new NotImplementedException("Command " + command + " not implemented");
}
} catch (Exception ex) {
throw new InternalException("Internal error: " + ex.getClass().getName());
}
}
@Override
public void run() {
// reset the classloader
OutputStream osm = null;
InputStream ism = null;
try {
LOG.fine("Opening output stream to master");
// will block, but this is necessary so the IDE eventually sets the debuggerKey
osm = socket.getOutputStream();
ism = socket.getInputStream();
// will read immediately
LOG.fine("Opening input stream from master");
Map<String, Consumer<OutputStream>> chans = new HashMap<>();
forwardExecutionControlAndIO(this, ism, osm, chans, Collections.emptyMap());
} catch (EOFException ex) {
// expected.
LOG.log(Level.FINE, "EOF", ex);
} catch (IOException ex) {
LOG.log(Level.FINE, "I/O", ex);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (osm != null) {
try {
osm.close();
} catch (IOException ex) {
// ignore
}
}
if (ism != null) {
try {
ism.close();
} catch (IOException ex) {
// ignore
}
}
}
}
/**
* Collect and send information about the executing VM.
*/
public static final int CMD_VM_INFO = 100;
public static final int CMD_REDEFINE = 101;
public static final int CMD_STOP = 102;
public static final int CMD_CLASSID = 103;
/**
* Find out reference identity of a class.
*/
public static final int CMD_TYPE_ID = 101;
private Pattern EXCLUDE_CLASSPATH_ITEMS = Pattern.compile(
"lib/tools.jar$|"
+ "modules/ext/nb-custom-jshell-probe.jar$"
);
private Object returnVMInfo(ObjectOutput o) throws IOException {
Map<String, String> result = new HashMap<String, String>();
Properties props = System.getProperties();
for (String s : props.stringPropertyNames()) {
if (!s.startsWith("java")) { // NOI18N
continue;
}
result.put(s, props.getProperty(s));
}
LOG.log(Level.FINE, "Sending properties: " + props);
StringBuilder cp = new StringBuilder();
for (URL u: loader.getURLs()) {
try {
File f = new File(u.toURI());
String s = f.getPath();
if (EXCLUDE_CLASSPATH_ITEMS.matcher(s).find()) {
continue;
}
if (cp.length() > 0) {
cp.append(":"); // NOI18N
}
cp.append(f.getPath());
} catch (URISyntaxException ex) {
cp.append(u.toExternalForm());
}
}
for (String s : props.getProperty("java.class.path").split(File.pathSeparator)) { // NOI18N
if (s.isEmpty()) {
continue;
}
if (EXCLUDE_CLASSPATH_ITEMS.matcher(s).find()) {
continue;
}
if (cp.length() > 0) {
cp.append(":"); // NOI18N
}
cp.append(s);
}
LOG.log(Level.FINE, "Classloader path: " + cp);
result.put("nb.class.path", cp.toString()); // NOI18N
return result;
}
private ClassLoader contextLoader;
protected void clientCodeEnter() {
LOG.log(Level.FINER, "Entering client code");
this.contextLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(loader);
super.clientCodeEnter();
}
private AtomicBoolean killed = new AtomicBoolean();
// private boolean killed;
/**
* Removes our thread from the user-executing map.
* It's important that clientCodeLeave is called BEFORE the agent starts to send
* response code: either the thread is removed here, and the response code is sent by
* the original Agent's code, or the entry is removed by the killer agent before/while
* executing agent is waiting to lock the map - and in that case the `killed' will be set
* to true and response code is produced in performExecute. Since ThreadDeath is thrown,
* the executing agent will not complete the synchronized block normally
*/
protected void clientCodeLeave() throws InternalException {
super.clientCodeLeave();
LOG.log(Level.FINER, "Exiting client code");
Thread.currentThread().setContextClassLoader(contextLoader);
synchronized (userCodeExecutingThreads) {
Thread t = userCodeExecutingThreads.get(socketPort);
if (t == Thread.currentThread()) {
killed.set(userCodeExecutingThreads.remove(socketPort) != null);
}
LOG.log(Level.FINER, "User code killed: {0}", killed);
}
}
@Override
public String invoke(String className, String methodName) throws RunException, InternalException, EngineTerminationException {
final Exception [] err = new Exception[1];
final CountDownLatch execLatch = new CountDownLatch(1);
final String[] result = new String[1];
// for Graalists :)
userExecutor.execute(new Runnable() {
public void run() {
try {
result[0] = performExecute(className, methodName, execLatch);
} catch (Exception ex) {
err[0] = ex;
}
}
});
try {
execLatch.await();
} catch (InterruptedException ex) {
throw new StoppedException();
}
if (err[0] != null) {
try {
throw err[0];
} catch (RunException | InternalException | EngineTerminationException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalException("InternalException: " + ex.getClass().getName());
}
}
return result[0];
}
private String performExecute(String className, String methodName, CountDownLatch latch)
throws ExecutionControlException {
killed.set(false);
try {
synchronized (userCodeExecutingThreads) {
userCodeExecutingThreads.put(socketPort, Thread.currentThread());
}
return super.invoke(className, methodName);
} catch (ThreadDeath td) {
LOG.log(Level.FINE, "Received ThreadDeath, killed: {0}", killed);
if (!killed.get()) {
throw td;
}
} catch (ExecutionControlException ex) {
throw ex;
} catch (Throwable t) {
killed.set(true);
} finally {
latch.countDown();
}
throw new StoppedException();
}
private void performStop(int agentId) throws ExecutionControlException {
Thread targetThread;
if (agentId == -1) {
agentId = this.socketPort;
}
synchronized (userCodeExecutingThreads) {
targetThread = userCodeExecutingThreads.remove(agentId);
if (targetThread != null) {
// throw ThreadDeath in the target thread
targetThread.stop();
}
}
if (targetThread == null) {
throw new InternalException("Invalid agent ID or not executing user code");
}
}
private List<URL> additionalClasspath = new ArrayList<>();
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return prepareClassLoader().findClass(name);
}
@Override
public void addToClasspath(String cp) throws EngineTerminationException, InternalException {
for (String cpItem : cp.split(";")) { // NOI18N
File f = new File(cpItem);
if (!f.isAbsolute()) {
throw new InternalException("Relative paths unuspported yet"); // NOI18N
}
try {
URL url = f.toURI().toURL();
additionalClasspath.add(url);
if (loader != null) {
loader.addURL(url);
}
} catch (MalformedURLException ex) {
throw new InternalException("Invalid file url: " + cpItem);
}
}
// reset the classloader
lastClassLoader = null;
}
@Override
public void load(ClassBytecodes[] cbcs) throws ClassInstallException, NotImplementedException, EngineTerminationException {
int count = cbcs.length;
boolean[] status = new boolean[cbcs.length];
int success = 0;
NbRemoteLoader ldr = prepareClassLoader();
for (int i = 0; i < count; i++) {
String name = cbcs[i].name();
byte[] byteCode = cbcs[i].bytecodes();
ldr.delare(name, byteCode);
try {
Class.forName(name, false, ldr);
success++;
status[i] = true;
} catch (ClassNotFoundException ex) {
Logger.getLogger(AgentWorker.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (success < count) {
throw new ClassInstallException("Could not define class", status);
}
}
@Override
public void redefine(ClassBytecodes[] cbcs) throws ClassInstallException, NotImplementedException, EngineTerminationException {
int count = cbcs.length;
boolean[] status = new boolean[cbcs.length];
int success = 0;
NbRemoteLoader ldr = prepareClassLoader();
for (int i = 0; i < count; i++) {
String name = cbcs[i].name();
byte[] replaceBytecode = cbcs[i].bytecodes();
Long id = ldr.getClassId(name);
if (id != null) {
Class defined = ldr.getClassOfId(id);
try {
agent.getInstrumentation().redefineClasses(
new ClassDefinition(defined, replaceBytecode)
);
status[i] = true;
success++;
} catch (ClassNotFoundException | UnmodifiableClassException ex) {
Logger.getLogger(AgentWorker.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (success < count) {
throw new ClassInstallException("Could not redefine classes", status);
}
}
}
private void performClassId(ObjectInputStream in, ObjectOutputStream out) throws IOException {
String className = in.readUTF();
Long id = ((NbRemoteLoader)loader).getClassId(className);
out.writeInt(RemoteCodes.RESULT_SUCCESS);
if (id == null) {
out.writeLong(-1);
} else {
out.writeLong(id);
}
out.flush();
}
}