blob: bace77581c8090bf0563f622d6efc4d8f5b453e3 [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 jdk.jshell.spi.SPIResolutionException;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import static org.netbeans.lib.jshell.agent.RemoteCodes.*;
import java.util.Map;
import java.util.TreeMap;
/**
* The remote agent runs in the execution process (separate from the main JShell
* process. This agent loads code over a socket from the main JShell process,
* executes the code, and other misc,
* @author Robert Field
*/
class RemoteAgent {
protected RemoteClassLoader loader = new RemoteClassLoader();
private final Map<String, Class<?>> klasses = new TreeMap<String, Class<?>>();
public static void main(String[] args) throws Exception {
String loopBack = null;
Socket socket = new Socket(loopBack, Integer.parseInt(args[0]));
(new RemoteAgent()).commandLoop(socket);
}
void commandLoop(Socket socket) throws IOException {
// in before out -- so we don't hang the controlling process
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
OutputStream socketOut = socket.getOutputStream();
System.setOut(new PrintStream(new MultiplexingOutputStream("out", socketOut), true));
System.setErr(new PrintStream(new MultiplexingOutputStream("err", socketOut), true));
ObjectOutputStream out = new ObjectOutputStream(new MultiplexingOutputStream("command", socketOut));
while (true) {
int cmd = in.readInt();
performCommand(cmd, in, out);
}
}
protected void performCommand(int cmd, ObjectInputStream in, ObjectOutputStream out) throws IOException {
switch (cmd) {
case CMD_EXIT:
// Terminate this process
return;
case CMD_LOAD:
// Load a generated class file over the wire
try {
int count = in.readInt();
List<String> names = new ArrayList<String>(count);
for (int i = 0; i < count; ++i) {
String name = in.readUTF();
byte[] kb = (byte[]) in.readObject();
loader.delare(name, kb);
names.add(name);
}
for (String name : names) {
Class<?> klass = loader.loadClass(name);
klasses.put(name, klass);
// Get class loaded to the point of, at least, preparation
klass.getDeclaredMethods();
}
out.writeInt(RESULT_SUCCESS);
out.flush();
} catch (IOException ex) {
handleLoadFailure(ex, out);
} catch (ClassNotFoundException ex) {
handleLoadFailure(ex, out);
} catch (ClassCastException ex) {
handleLoadFailure(ex, out);
}
break;
case CMD_INVOKE: {
// Invoke executable entry point in loaded code
String name = in.readUTF();
Class<?> klass = klasses.get(name);
if (klass == null) {
debug("*** Invoke failure: no such class loaded %s\n", name);
out.writeInt(RESULT_FAIL);
out.writeUTF("no such class loaded: " + name);
out.flush();
break;
}
String methodName = in.readUTF();
Method doitMethod;
try {
//this.getClass().getModule().addExports(SPIResolutionException.class.getPackage().getName(), klass.getModule());
doitMethod = klass.getDeclaredMethod(methodName, new Class<?>[0]);
doitMethod.setAccessible(true);
Object res;
try {
clientCodeEnter();
res = doitMethod.invoke(null, new Object[0]);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof ThreadDeath) {
expectingStop = false;
throw (ThreadDeath) ex.getCause();
}
throw ex;
} catch (StopExecutionException ex) {
expectingStop = false;
throw ex;
} finally {
clientCodeLeave();
}
out.writeInt(RESULT_SUCCESS);
out.writeUTF(valueString(res));
out.flush();
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
StackTraceElement[] elems = cause.getStackTrace();
if (cause instanceof SPIResolutionException) {
out.writeInt(RESULT_CORRALLED);
out.writeInt(((SPIResolutionException) cause).id());
} else {
out.writeInt(RESULT_EXCEPTION);
out.writeUTF(cause.getClass().getName());
out.writeUTF(cause.getMessage() == null ? "<none>" : cause.getMessage());
}
out.writeInt(elems.length);
for (StackTraceElement ste : elems) {
out.writeUTF(ste.getClassName());
out.writeUTF(ste.getMethodName());
out.writeUTF(ste.getFileName() == null ? "<none>" : ste.getFileName());
out.writeInt(ste.getLineNumber());
}
out.flush();
} catch (NoSuchMethodException ex) {
handleInvocationFailure(ex, out);
} catch (IllegalAccessException ex) {
handleInvocationFailure(ex, out);
} catch (StopExecutionException ex) {
try {
out.writeInt(RESULT_KILLED);
out.flush();
} catch (IOException err) {
debug("*** Error writing killed result: %s -- %s\n", ex, ex.getCause());
}
}
System.out.flush();
break;
}
case CMD_VARVALUE: {
// Retrieve a variable value
String classname = in.readUTF();
String varname = in.readUTF();
Class<?> klass = klasses.get(classname);
if (klass == null) {
debug("*** Var value failure: no such class loaded %s\n", classname);
out.writeInt(RESULT_FAIL);
out.writeUTF("no such class loaded: " + classname);
out.flush();
break;
}
try {
Field var = klass.getDeclaredField(varname);
var.setAccessible(true);
Object res = var.get(null);
out.writeInt(RESULT_SUCCESS);
out.writeUTF(valueString(res));
out.flush();
} catch (Exception ex) {
debug("*** Var value failure: no such field %s.%s\n", classname, varname);
out.writeInt(RESULT_FAIL);
out.writeUTF("no such field loaded: " + varname + " in class: " + classname);
out.flush();
}
break;
}
case CMD_CLASSPATH: {
// Append to the claspath
String cp = in.readUTF();
for (String path : cp.split(File.pathSeparator)) {
loader.addURL(new File(path).toURI().toURL());
}
out.writeInt(RESULT_SUCCESS);
out.flush();
break;
}
default:
debug("*** Bad command code: %d\n", cmd);
break;
}
}
private void handleLoadFailure(Throwable ex, ObjectOutputStream out) throws IOException {
debug("*** Load failure: %s\n", ex);
out.writeInt(RESULT_FAIL);
out.writeUTF(ex.toString());
out.flush();
}
private void handleInvocationFailure(Throwable ex, ObjectOutputStream out) throws IOException {
debug("*** Invoke failure: %s -- %s\n", ex, ex.getCause());
out.writeInt(RESULT_FAIL);
out.writeUTF(ex.toString());
out.flush();
}
protected void handleUnknownCommand(int cmd, ObjectInputStream i, ObjectOutputStream o) throws IOException {
debug("*** Bad command code: %d\n", cmd);
}
// These three variables are used by the main JShell process in interrupting
// the running process. Access is via JDI, so the reference is not visible
// to code inspection.
private boolean inClientCode; // Queried by the main process
private boolean expectingStop; // Set by the main process
// thrown by the main process via JDI:
private final StopExecutionException stopException = new StopExecutionException();
@SuppressWarnings("serial") // serialVersionUID intentionally omitted
private class StopExecutionException extends ThreadDeath {
@Override public synchronized Throwable fillInStackTrace() {
return this;
}
}
void clientCodeEnter() {
expectingStop = false;
inClientCode = true;
}
void clientCodeLeave() {
inClientCode = false;
while (expectingStop) {
try {
Thread.sleep(0);
} catch (InterruptedException ex) {
debug("*** Sleep interrupted while waiting for stop exception: %s\n", ex);
}
}
}
private void debug(String format, Object... args) {
System.err.printf("REMOTE: "+format, args);
}
static String valueString(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + (String)value + "\"";
} else if (value instanceof Character) {
return "'" + value + "'";
} else {
return value.toString();
}
}
static final class MultiplexingOutputStream extends OutputStream {
private static final int PACKET_SIZE = 127;
private final byte[] name;
private final OutputStream delegate;
public MultiplexingOutputStream(String name, OutputStream delegate) {
try {
this.name = name.getBytes("UTF-8");
this.delegate = delegate;
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex); //should not happen
}
}
@Override
public void write(int b) throws IOException {
synchronized (delegate) {
delegate.write(name.length); //assuming the len is small enough to fit into byte
delegate.write(name);
delegate.write(1);
delegate.write(b);
delegate.flush();
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
synchronized (delegate) {
int i = 0;
while (len > 0) {
int size = Math.min(PACKET_SIZE, len);
delegate.write(name.length); //assuming the len is small enough to fit into byte
delegate.write(name);
delegate.write(size);
delegate.write(b, off + i, size);
i += size;
len -= size;
}
delegate.flush();
}
}
@Override
public void flush() throws IOException {
super.flush();
delegate.flush();
}
@Override
public void close() throws IOException {
super.close();
delegate.close();
}
}
}