/*
* Copyright 2014 - 2019 Rafael Winterhalter
*
* Licensed 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 net.bytebuddy.agent;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
An implementation for attachment on a virtual machine. This interface mimics the tooling API's virtual machine interface to allow for similar usage by ByteBuddyAgent
where all calls are made via reflection such that this structural typing suffices for interoperability.
Note: Implementations are required to declare a static method attach(String)
returning an instance of a class that declares the methods defined by VirtualMachine
.
/**
* <p>
* An implementation for attachment on a virtual machine. This interface mimics the tooling API's virtual
* machine interface to allow for similar usage by {@link ByteBuddyAgent} where all calls are made via
* reflection such that this structural typing suffices for interoperability.
* </p>
* <p>
* <b>Note</b>: Implementations are required to declare a static method {@code attach(String)} returning an
* instance of a class that declares the methods defined by {@link VirtualMachine}.
* </p>
*/
public interface VirtualMachine {
Loads an agent into the represented virtual machine.
Params: - jarFile – The jar file to attach.
- argument – The argument to provide or
null
if no argument should be provided.
Throws: - IOException – If an I/O exception occurs.
/**
* Loads an agent into the represented virtual machine.
*
* @param jarFile The jar file to attach.
* @param argument The argument to provide or {@code null} if no argument should be provided.
* @throws IOException If an I/O exception occurs.
*/
@SuppressWarnings("unused")
void loadAgent(String jarFile, String argument) throws IOException;
Detaches this virtual machine representation.
Throws: - IOException – If an I/O exception occurs.
/**
* Detaches this virtual machine representation.
*
* @throws IOException If an I/O exception occurs.
*/
@SuppressWarnings("unused")
void detach() throws IOException;
A virtual machine implementation for a HotSpot VM or any compatible VM.
/**
* A virtual machine implementation for a HotSpot VM or any compatible VM.
*/
abstract class ForHotSpot implements VirtualMachine {
The UTF-8 charset name.
/**
* The UTF-8 charset name.
*/
private static final String UTF_8 = "UTF-8";
The protocol version to use for communication.
/**
* The protocol version to use for communication.
*/
private static final String PROTOCOL_VERSION = "1";
The load
command. /**
* The {@code load} command.
*/
private static final String LOAD_COMMAND = "load";
The instrument
command. /**
* The {@code instrument} command.
*/
private static final String INSTRUMENT_COMMAND = "instrument";
A delimiter to be used for attachment.
/**
* A delimiter to be used for attachment.
*/
private static final String ARGUMENT_DELIMITER = "=";
A blank line argument.
/**
* A blank line argument.
*/
private static final byte[] BLANK = new byte[]{0};
The target process's id.
/**
* The target process's id.
*/
protected final String processId;
Creates a new HotSpot-compatible VM implementation.
Params: - processId – The target process's id.
/**
* Creates a new HotSpot-compatible VM implementation.
*
* @param processId The target process's id.
*/
protected ForHotSpot(String processId) {
this.processId = processId;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public void loadAgent(String jarFile, String argument) throws IOException {
connect();
write(PROTOCOL_VERSION.getBytes(UTF_8));
write(BLANK);
write(LOAD_COMMAND.getBytes(UTF_8));
write(BLANK);
write(INSTRUMENT_COMMAND.getBytes(UTF_8));
write(BLANK);
write(Boolean.FALSE.toString().getBytes(UTF_8));
write(BLANK);
write((argument == null
? jarFile
: jarFile + ARGUMENT_DELIMITER + argument).getBytes(UTF_8));
write(BLANK);
byte[] buffer = new byte[1];
StringBuilder stringBuilder = new StringBuilder();
int length;
while ((length = read(buffer)) != -1) {
if (length > 0) {
if (buffer[0] == 10) {
break;
}
stringBuilder.append((char) buffer[0]);
}
}
switch (Integer.parseInt(stringBuilder.toString())) {
case 0:
return;
case 101:
throw new IOException("Protocol mismatch with target VM");
default:
buffer = new byte[1024];
stringBuilder = new StringBuilder();
while ((length = read(buffer)) != -1) {
stringBuilder.append(new String(buffer, 0, length, UTF_8));
}
throw new IllegalStateException(stringBuilder.toString());
}
}
Connects to the target VM.
Throws: - IOException – If an I/O exception occurs.
/**
* Connects to the target VM.
*
* @throws IOException If an I/O exception occurs.
*/
protected abstract void connect() throws IOException;
Reads from the communication channel.
Params: - buffer – The buffer to read into.
Throws: - IOException – If an I/O exception occurs.
Returns: The amount of bytes read.
/**
* Reads from the communication channel.
*
* @param buffer The buffer to read into.
* @return The amount of bytes read.
* @throws IOException If an I/O exception occurs.
*/
protected abstract int read(byte[] buffer) throws IOException;
Writes to the communication channel.
Params: - buffer – The buffer to write from.
Throws: - IOException – If an I/O exception occurs.
/**
* Writes to the communication channel.
*
* @param buffer The buffer to write from.
* @throws IOException If an I/O exception occurs.
*/
protected abstract void write(byte[] buffer) throws IOException;
A virtual machine implementation for a HotSpot VM running on Unix.
/**
* A virtual machine implementation for a HotSpot VM running on Unix.
*/
public static class OnUnix extends ForHotSpot {
The default amount of attempts to connect.
/**
* The default amount of attempts to connect.
*/
private static final int DEFAULT_ATTEMPTS = 10;
The default pause between two attempts.
/**
* The default pause between two attempts.
*/
private static final long DEFAULT_PAUSE = 200;
The default socket timeout.
/**
* The default socket timeout.
*/
private static final long DEFAULT_TIMEOUT = 5000;
The temporary directory on Unix systems.
/**
* The temporary directory on Unix systems.
*/
private static final String TEMPORARY_DIRECTORY = "/tmp";
The name prefix for a socket.
/**
* The name prefix for a socket.
*/
private static final String SOCKET_FILE_PREFIX = ".java_pid";
The name prefix for an attachment file indicator.
/**
* The name prefix for an attachment file indicator.
*/
private static final String ATTACH_FILE_PREFIX = ".attach_pid";
The Unix socket to use for communication. The containing object is supposed to be an instance of AFUNIXSocket
which is however not set to avoid eager loading /**
* The Unix socket to use for communication. The containing object is supposed to be an instance
* of {@link AFUNIXSocket} which is however not set to avoid eager loading
*/
private final Object socket;
The number of attempts to connect.
/**
* The number of attempts to connect.
*/
private final int attempts;
The time to pause between attempts.
/**
* The time to pause between attempts.
*/
private final long pause;
The socket timeout.
/**
* The socket timeout.
*/
private final long timeout;
The time unit of the pause time.
/**
* The time unit of the pause time.
*/
private final TimeUnit timeUnit;
Creates a new VM implementation for a HotSpot VM running on Unix.
Params: - processId – The process id of the target VM.
- socket – The Unix socket to use for communication.
- attempts – The number of attempts to connect.
- pause – The pause time between two VMs.
- timeout – The socket timeout.
- timeUnit – The time unit of the pause time.
/**
* Creates a new VM implementation for a HotSpot VM running on Unix.
*
* @param processId The process id of the target VM.
* @param socket The Unix socket to use for communication.
* @param attempts The number of attempts to connect.
* @param pause The pause time between two VMs.
* @param timeout The socket timeout.
* @param timeUnit The time unit of the pause time.
*/
public OnUnix(String processId, Object socket, int attempts, long pause, long timeout, TimeUnit timeUnit) {
super(processId);
this.socket = socket;
this.attempts = attempts;
this.pause = pause;
this.timeout = timeout;
this.timeUnit = timeUnit;
}
Asserts the availability of this virtual machine implementation. If the Unix socket library is missing or if this VM does not support Unix socket communication, a Throwable
is thrown. Throws: - Throwable – If this attachment method is not available.
Returns: This virtual machine type.
/**
* Asserts the availability of this virtual machine implementation. If the Unix socket library is missing or
* if this VM does not support Unix socket communication, a {@link Throwable} is thrown.
*
* @return This virtual machine type.
* @throws Throwable If this attachment method is not available.
*/
public static Class<?> assertAvailability() throws Throwable {
try {
Class<?> moduleType = Class.forName("java.lang.Module");
Method getModule = Class.class.getMethod("getModule"), canRead = moduleType.getMethod("canRead", moduleType);
Object thisModule = getModule.invoke(OnUnix.class), otherModule = getModule.invoke(AFUNIXSocket.class);
if (!(Boolean) canRead.invoke(thisModule, otherModule)) {
moduleType.getMethod("addReads", moduleType).invoke(thisModule, otherModule);
}
return doAssertAvailability();
} catch (ClassNotFoundException ignored) {
return doAssertAvailability();
}
}
Asserts the availability of this virtual machine implementation.
Returns: This virtual machine type.
/**
* Asserts the availability of this virtual machine implementation.
*
* @return This virtual machine type.
*/
private static Class<?> doAssertAvailability() {
if (!AFUNIXSocket.isSupported()) {
throw new IllegalStateException("POSIX sockets are not supported on the current system");
} else if (!System.getProperty("java.vm.name").toLowerCase(Locale.US).contains("hotspot")) {
throw new IllegalStateException("Cannot apply attachment on non-Hotspot compatible VM");
} else {
return OnUnix.class;
}
}
Attaches to the supplied VM process.
Params: - processId – The process id of the target VM.
Throws: - IOException – If an I/O exception occurs.
Returns: An appropriate virtual machine implementation.
/**
* Attaches to the supplied VM process.
*
* @param processId The process id of the target VM.
* @return An appropriate virtual machine implementation.
* @throws IOException If an I/O exception occurs.
*/
public static VirtualMachine attach(String processId) throws IOException {
return new OnUnix(processId, AFUNIXSocket.newInstance(), DEFAULT_ATTEMPTS, DEFAULT_PAUSE, DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", justification = "This is a Unix-specific implementation")
protected void connect() throws IOException {
File socketFile = new File(TEMPORARY_DIRECTORY, SOCKET_FILE_PREFIX + processId);
if (!socketFile.exists()) {
String target = ATTACH_FILE_PREFIX + processId, path = "/proc/" + processId + "/cwd/" + target;
File attachFile = new File(path);
try {
if (!attachFile.createNewFile() && !attachFile.isFile()) {
throw new IllegalStateException("Could not create attach file: " + attachFile);
}
} catch (IOException ignored) {
attachFile = new File(TEMPORARY_DIRECTORY, target);
if (!attachFile.createNewFile() && !attachFile.isFile()) {
throw new IllegalStateException("Could not create attach file: " + attachFile);
}
}
try {
// The HotSpot attachment API attempts to send the signal to all children of a process
Process process = Runtime.getRuntime().exec("kill -3 " + processId);
int attempts = this.attempts;
boolean killed = false;
do {
try {
if (process.exitValue() != 0) {
throw new IllegalStateException("Error while sending signal to target VM: " + processId);
}
killed = true;
break;
} catch (IllegalThreadStateException ignored) {
attempts -= 1;
Thread.sleep(timeUnit.toMillis(pause));
}
} while (attempts > 0);
if (!killed) {
throw new IllegalStateException("Target VM did not respond to signal: " + processId);
}
attempts = this.attempts;
while (attempts-- > 0 && !socketFile.exists()) {
Thread.sleep(timeUnit.toMillis(pause));
}
if (!socketFile.exists()) {
throw new IllegalStateException("Target VM did not respond: " + processId);
}
} catch (InterruptedException exception) {
throw new IllegalStateException("Interrupted during wait for process", exception);
} finally {
if (!attachFile.delete()) {
attachFile.deleteOnExit();
}
}
}
if (timeout != 0) {
((AFUNIXSocket) socket).setSoTimeout((int) timeUnit.toMillis(timeout));
}
((AFUNIXSocket) socket).connect(new AFUNIXSocketAddress(socketFile));
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public int read(byte[] buffer) throws IOException {
return ((AFUNIXSocket) this.socket).getInputStream().read(buffer);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public void write(byte[] buffer) throws IOException {
((AFUNIXSocket) this.socket).getOutputStream().write(buffer);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public void detach() throws IOException {
((AFUNIXSocket) this.socket).close();
}
}
}
}