/*
* Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the Classpath exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.btrace.client;
import com.sun.btrace.BTraceRuntime;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.net.URI;
import java.util.Map;
import com.sun.btrace.CommandListener;
import com.sun.btrace.SharedSettings;
import com.sun.btrace.compiler.Compiler;
import com.sun.btrace.annotations.DTrace;
import com.sun.btrace.annotations.DTraceRef;
import com.sun.btrace.comm.Command;
import com.sun.btrace.comm.EventCommand;
import com.sun.btrace.comm.ExitCommand;
import com.sun.btrace.comm.InstrumentCommand;
import com.sun.btrace.comm.MessageCommand;
import com.sun.btrace.comm.SetSettingsCommand;
import com.sun.btrace.comm.WireIO;
import com.sun.btrace.org.objectweb.asm.*;
import com.sun.tools.attach.VirtualMachine;
import java.net.ConnectException;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class represents a BTrace client. This can be
* used to create command line as well as a GUI based
* BTrace clients. The BTrace compilation, traced JVM attach,
* submission of compiled program and and I/O to the traced
* JVM are handled by this class.
*
* @author A. Sundararajan
*/
public class Client {
private static boolean dtraceEnabled;
private static Method submitFile;
private static Method submitString;
private static final String DTRACE_DESC;
private static final String DTRACE_REF_DESC;
static {
try {
/*
* Check for DTrace Consumer class -- if we don't have that
* either /usr/lib/share/java/dtrace.jar is not in CLASSPATH
* or we are not running on Solaris 11+.
*/
Class dtraceConsumerClass = Class.forName("org.opensolaris.os.dtrace.Consumer");
/*
* Check for BTrace's DTrace support class -- if that is available
* may be the user didn't build BTrace on Solaris 11. S/he built
* it on Solaris 10 or below or some other OS.
*/
Class dtraceClass = Class.forName("com.sun.btrace.dtrace.DTrace");
dtraceEnabled = true;
submitFile = dtraceClass.getMethod("submit",
new Class[]{File.class, String[].class,
CommandListener.class
});
submitString = dtraceClass.getMethod("submit",
new Class[]{String.class, String[].class,
CommandListener.class
});
} catch (Exception exp) {
dtraceEnabled = false;
}
DTRACE_DESC = Type.getDescriptor(DTrace.class);
DTRACE_REF_DESC = Type.getDescriptor(DTraceRef.class);
}
// port on which BTrace agent listens
private final int port;
// the output file or null
private final String outputFile;
// are we running debug mode?
private final boolean debug;
// do we need to track retransforming single classes? (will impose additional overhead)
private final boolean trackRetransforms;
// are we running in trusted mode?
private final boolean trusted;
// are we dumping .class files of
// the instrumented classes?
private final boolean dumpClasses;
// which directory we dump the .class files?
private final String dumpDir;
private final String probeDescPath;
private final String statsdDef;
// connection state to the traced JVM
private volatile Socket sock;
private volatile ObjectInputStream ois;
private volatile ObjectOutputStream oos;
public Client(int port) {
this(port, null, ".", false, false, false, false, null, null);
}
public Client(int port, String probeDescPath) {
this(port, null, probeDescPath, false, false, false, false, null, null);
}
public Client(int port, String outputFile, String probeDescPath,
boolean debug, boolean trackRetransforms,
boolean trusted, boolean dumpClasses,
String dumpDir, String statsdDef) {
this.port = port;
this.outputFile = outputFile;
this.probeDescPath = probeDescPath;
this.debug = debug;
this.trusted = trusted;
this.dumpClasses = dumpClasses;
this.dumpDir = dumpDir;
this.trackRetransforms = trackRetransforms;
this.statsdDef = statsdDef;
}
@SuppressWarnings("DefaultCharset")
public byte[] compile(String fileName, String classPath) {
return compile(fileName, classPath, new PrintWriter(System.err), null);
}
/**
* Compiles given BTrace program using given classpath.
*/
@SuppressWarnings("DefaultCharset")
public byte[] compile(String fileName, String classPath, String includePath) {
return compile(fileName, classPath, new PrintWriter(System.err), includePath);
}
public byte[] compile(String fileName, String classPath,
PrintWriter err) {
return compile(fileName, classPath, err, null);
}
/**
* Compiles given BTrace program using given classpath.
* Errors and warning are written to given PrintWriter.
*/
public byte[] compile(String fileName, String classPath,
PrintWriter err, String includePath) {
byte[] code = null;
File file = new File(fileName);
if (fileName.endsWith(".java")) {
Compiler compiler = new Compiler(includePath);
classPath += File.pathSeparator + System.getProperty("java.class.path");
if (debug) {
debugPrint("compiling " + fileName);
}
Map<String, byte[]> classes = compiler.compile(file,
err, ".", classPath);
if (classes == null) {
err.println("btrace compilation failed!");
return null;
}
int size = classes.size();
if (size != 1) {
err.println("no classes or more than one class");
return null;
}
String name = classes.keySet().iterator().next();
code = classes.get(name);
if (debug) {
debugPrint("compiled " + fileName);
}
} else if (fileName.endsWith(".class")) {
int codeLen = (int)file.length();
code = new byte[codeLen];
try {
if (debug) {
debugPrint("reading " + fileName);
}
try (FileInputStream fis = new FileInputStream(file)) {
int off = 0;
int len = 0;
do {
len = fis.read(code, off, codeLen - off);
if (len > -1) {
off += len;
}
} while (off < codeLen && len != -1);
}
if (debug) {
debugPrint("read " + fileName);
}
} catch (IOException exp) {
err.println(exp.getMessage());
return null;
}
} else {
err.println("BTrace script has to be a .java or a .class");
return null;
}
return code;
}
/**
* Attach the BTrace client to the given Java process.
* Loads BTrace agent on the target process if not loaded
* already.
*/
public void attach(String pid, String sysCp, String bootCp) throws IOException {
try {
String agentPath = "/btrace-agent.jar";
String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
tmp = tmp.substring(0, tmp.indexOf('!'));
tmp = tmp.substring("jar:".length(), tmp.lastIndexOf('/'));
agentPath = tmp + agentPath;
agentPath = new File(new URI(agentPath)).getAbsolutePath();
attach(pid, agentPath, sysCp, bootCp);
} catch (RuntimeException re) {
throw re;
} catch (IOException ioexp) {
throw ioexp;
} catch (Exception exp) {
throw new IOException(exp.getMessage());
}
}
/**
* Attach the BTrace client to the given Java process.
* Loads BTrace agent on the target process if not loaded
* already. Accepts the full path of the btrace agent jar.
* Also, accepts system classpath and boot classpath optionally.
*/
public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
try {
VirtualMachine vm = null;
if (debug) {
debugPrint("attaching to " + pid);
}
vm = VirtualMachine.attach(pid);
if (debug) {
debugPrint("checking port availability: " + port);
}
Properties serverVmProps = vm.getSystemProperties();
int serverPort = Integer.parseInt(serverVmProps.getProperty("btrace.port", "-1"));
if (serverPort != -1) {
if (serverPort != port) {
throw new IOException("Can not attach to PID " + pid + " on port " + port + ". There is already a BTrace server active on port " + serverPort + "!");
}
} else {
if (!isPortAvailable(port)) {
throw new IOException("Port " + port + " unavailable.");
}
}
if (debug) {
debugPrint("attached to " + pid);
}
if (debug) {
debugPrint("loading " + agentPath);
}
String agentArgs = "port=" + port;
if (statsdDef != null) {
agentArgs += ",statsd=" + statsdDef;
}
if (debug) {
agentArgs += ",debug=true";
}
if (trusted) {
agentArgs += ",trusted=true";
}
if (dumpClasses) {
agentArgs += ",dumpClasses=true";
agentArgs += ",dumpDir=" + dumpDir;
}
if (trackRetransforms) {
agentArgs += ",trackRetransforms=true";
}
if (bootCp != null) {
agentArgs += ",bootClassPath=" + bootCp;
}
String toolsPath = getToolsJarPath(
serverVmProps.getProperty("java.class.path"),
serverVmProps.getProperty("java.home")
);
if (sysCp == null) {
sysCp = toolsPath;
} else {
sysCp = sysCp + File.pathSeparator + toolsPath;
}
agentArgs += ",systemClassPath=" + sysCp;
String cmdQueueLimit = System.getProperty(BTraceRuntime.CMD_QUEUE_LIMIT_KEY, null);
if (cmdQueueLimit != null) {
agentArgs += ",cmdQueueLimit=" + cmdQueueLimit;
}
agentArgs += ",probeDescPath=" + probeDescPath;
if (debug) {
debugPrint("agent args: " + agentArgs);
}
vm.loadAgent(agentPath, agentArgs);
if (debug) {
debugPrint("loaded " + agentPath);
}
} catch (RuntimeException re) {
throw re;
} catch (IOException ioexp) {
throw ioexp;
} catch (Exception exp) {
throw new IOException(exp.getMessage());
}
}
/**
* Submits the compiled BTrace .class to the VM
* attached and passes given command line arguments.
* Receives commands from the traced JVM and sends those
* to the command listener provided.
*/
public void submit(String fileName, byte[] code, String[] args,
CommandListener listener) throws IOException {
if (sock != null) {
throw new IllegalStateException();
}
submitDTrace(fileName, code, args, listener);
try {
if (debug) {
debugPrint("opening socket to " + port);
}
long timeout = System.currentTimeMillis() + 5000;
while (sock == null && System.currentTimeMillis() <= timeout) {
try {
sock = new Socket("localhost", port);
} catch (ConnectException e) {
if (debug) {
debugPrint("server not yet available; retrying ...");
}
Thread.sleep(20);
}
}
oos = new ObjectOutputStream(sock.getOutputStream());
if (debug) {
debugPrint("setting up client settings");
}
Map<String, Object> settings = new HashMap<>();
settings.put(SharedSettings.DEBUG_KEY, debug);
settings.put(SharedSettings.DUMP_DIR_KEY, dumpClasses ? dumpDir : "");
settings.put(SharedSettings.TRACK_RETRANSFORMS_KEY, trackRetransforms);
settings.put(SharedSettings.TRUSTED_KEY, trusted);
settings.put(SharedSettings.PROBE_DESC_PATH_KEY, probeDescPath);
settings.put(SharedSettings.OUTPUT_FILE_KEY, outputFile);
WireIO.write(oos, new SetSettingsCommand(settings));
if (debug) {
debugPrint("sending instrument command");
}
WireIO.write(oos, new InstrumentCommand(code, args));
ois = new ObjectInputStream(sock.getInputStream());
if (debug) {
debugPrint("entering into command loop");
}
commandLoop(listener);
} catch (UnknownHostException uhe) {
throw new IOException(uhe);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Submits the compiled BTrace .class to the VM
* attached and passes given command line arguments.
* Receives commands from the traced JVM and sends those
* to the command listener provided.
*/
public void submit(byte[] code, String[] args,
CommandListener listener) throws IOException {
submit(null, code, args, listener);
}
/**
* Sends ExitCommand to the traced JVM.
*/
public void sendExit() throws IOException {
sendExit(0);
}
/**
* Sends ExitCommand to the traced JVM.
*/
public void sendExit(int code) throws IOException {
send(new ExitCommand(code));
}
/**
* Sends an EventCommand to the traced JVM.
*/
public void sendEvent() throws IOException {
sendEvent("");
}
/**
* Sends an EventCommand to the traced JVM.
*/
public void sendEvent(String name) throws IOException {
send(new EventCommand(name));
}
/**
* Closes all connection state to the traced JVM.
*/
public synchronized void close() throws IOException {
if (ois != null) {
ois.close();
}
if (oos != null) {
oos.close();
}
if (sock != null) {
sock.close();
}
reset();
}
/**
* reset the internal status of the client
*/
private void reset() {
sock = null;
ois = null;
oos = null;
}
//-- Internals only below this point
private String getToolsJarPath(String javaClassPath, String javaHome) {
// try to get absolute path of tools.jar
// first check this application's classpath
String[] components = javaClassPath.split(File.pathSeparator);
for (String c : components) {
if (c.endsWith("tools.jar")) {
return new File(c).getAbsolutePath();
} else if (c.endsWith("classes.jar")) { // MacOS specific
return new File(c).getAbsolutePath();
}
}
// we didn't find -- make a guess! If this app is running on a JDK rather
// than a JRE there will be a tools.jar in $JDK_HOME/lib directory.
if(System.getProperty("os.name").startsWith("Mac")) {
String java_mac_home = javaHome.substring(0,javaHome.indexOf("/Home"));
return java_mac_home + "/Home/lib/tools.jar";
} else {
return javaHome + "/../lib/tools.jar";
}
}
private void send(Command cmd) throws IOException {
if (oos == null) {
throw new IllegalStateException();
}
oos.reset();
WireIO.write(oos, cmd);
}
private void commandLoop(CommandListener listener)
throws IOException {
assert ois != null : "null input stream?";
final AtomicBoolean exited = new AtomicBoolean(false);
while (true) {
try {
Command cmd = WireIO.read(ois);
if (debug) {
debugPrint("received " + cmd);
}
listener.onCommand(cmd);
if (cmd.getType() == Command.EXIT) {
debugPrint("received EXIT cmd");
return;
}
} catch (IOException e) {
if (exited.compareAndSet(false, true)) listener.onCommand(new ExitCommand(-1));
throw e;
} catch (NullPointerException e) {
e.printStackTrace();
if (exited.compareAndSet(false, true))listener.onCommand(new ExitCommand(-1));
}
}
}
public void debugPrint(String msg) {
System.out.println("DEBUG: " + msg);
}
private void warn(CommandListener listener, String msg) {
try {
msg = "WARNING: " + msg + "\n";
listener.onCommand(new MessageCommand(msg));
} catch (IOException exp) {
if (debug) {
exp.printStackTrace();
}
}
}
private void submitDTrace(String fileName, byte[] code,
String[] args, CommandListener listener) {
if (fileName == null || code == null) {
return;
}
Object dtraceSrc = getDTraceSource(fileName, code);
try {
if (dtraceSrc instanceof String) {
if (dtraceEnabled) {
submitString.invoke(null, dtraceSrc, args, listener);
} else {
warn(listener, "@DTrace is supported only on Solaris 11+");
}
} else if (dtraceSrc instanceof File) {
if (dtraceEnabled) {
submitFile.invoke(null, dtraceSrc, args, listener);
} else {
warn(listener, "@DTraceRef is supported only on Solaris 11+");
}
}
} catch (IllegalAccessException | IllegalArgumentException iace) {
iace.printStackTrace();
} catch (InvocationTargetException ite) {
throw new RuntimeException(ite.getTargetException());
}
}
private Object getDTraceSource(final String fileName, byte[] code) {
ClassReader reader = new ClassReader(code);
final Object[] result = new Object[1];
reader.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean vis) {
if (desc.equals(DTRACE_DESC)) {
return new AnnotationVisitor(Opcodes.ASM5) {
@Override
public void visit(String name, Object value) {
if (name.equals("value")) {
result[0] = value;
}
}
};
} else if (desc.equals(DTRACE_REF_DESC)) {
return new AnnotationVisitor(Opcodes.ASM5) {
@Override
public void visit(String name, Object value) {
if (name.equals("value")) {
String tmp = value.toString();
File file = new File(tmp);
if (file.isAbsolute()) {
result[0] = file;
} else {
int index = fileName.lastIndexOf(File.separatorChar);
String dir;
if (index == -1) {
dir = ".";
} else {
dir = fileName.substring(0, index);
}
result[0] = new File(dir, tmp);
}
}
}
};
} else {
return super.visitAnnotation(desc, vis);
}
}
}, ClassReader.SKIP_CODE);
return result[0];
}
private static boolean isPortAvailable(int port) {
Socket clSocket = null;
try {
clSocket = new Socket("127.0.0.1", port);
} catch (UnknownHostException ex) {
} catch (IOException ex) {
clSocket = null;
}
if (clSocket != null) {
try {
clSocket.close();
} catch (IOException e) {
}
return false;
}
return true;
}
}