diff --git a/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Attacher.java b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Attacher.java new file mode 100644 index 000000000..608727e62 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Attacher.java @@ -0,0 +1,92 @@ +package com.fr.third.net.bytebuddy.agent; + +import java.lang.reflect.InvocationTargetException; + +/** + * A Java program that attaches a Java agent to an external process. + */ +public class Attacher { + + /** + * Base for access to a reflective member to make the code more readable. + */ + private static final Object STATIC_MEMBER = null; + + /** + * The name of the {@code attach} method of the {@code VirtualMachine} class. + */ + private static final String ATTACH_METHOD_NAME = "attach"; + + /** + * The name of the {@code loadAgent} method of the {@code VirtualMachine} class. + */ + private static final String LOAD_AGENT_METHOD_NAME = "loadAgent"; + + /** + * The name of the {@code detach} method of the {@code VirtualMachine} class. + */ + private static final String DETACH_METHOD_NAME = "detach"; + + /** + * Runs the attacher as a Java application. + * + * @param args A list containing the fully qualified name of the virtual machine type, + * the process id, the fully qualified name of the Java agent jar followed by + * an empty string if the argument to the agent is {@code null} or any number + * of strings where the first argument is proceeded by any single character + * which is stripped off. + */ + public static void main(String[] args) { + try { + String argument; + if (args.length < 4 || args[3].isEmpty()) { + argument = null; + } else { + StringBuilder stringBuilder = new StringBuilder(args[3].substring(1)); + for (int index = 4; index < args.length; index++) { + stringBuilder.append(' ').append(args[index]); + } + argument = stringBuilder.toString(); + } + install(Class.forName(args[0]), args[1], args[2], argument); + } catch (Exception ignored) { + System.exit(1); + } + } + + /** + * Installs a Java agent on a target VM. + * + * @param virtualMachineType The virtual machine type to use for the external attachment. + * @param processId The id of the process being target of the external attachment. + * @param agent The Java agent to attach. + * @param argument The argument to provide or {@code null} if no argument is provided. + * @throws NoSuchMethodException If the virtual machine type does not define an expected method. + * @throws InvocationTargetException If the virtual machine type raises an error. + * @throws IllegalAccessException If a method of the virtual machine type cannot be accessed. + */ + protected static void install(Class> virtualMachineType, + String processId, + String agent, + String argument) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Object virtualMachineInstance = virtualMachineType + .getMethod(ATTACH_METHOD_NAME, String.class) + .invoke(STATIC_MEMBER, processId); + try { + virtualMachineType + .getMethod(LOAD_AGENT_METHOD_NAME, String.class, String.class) + .invoke(virtualMachineInstance, agent, argument); + } finally { + virtualMachineType + .getMethod(DETACH_METHOD_NAME) + .invoke(virtualMachineInstance); + } + } + + /** + * The attacher provides only {@code static} utility methods and should not be instantiated. + */ + private Attacher() { + throw new UnsupportedOperationException(); + } +} diff --git a/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/ByteBuddyAgent.java b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/ByteBuddyAgent.java new file mode 100644 index 000000000..765db5d22 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/ByteBuddyAgent.java @@ -0,0 +1,1199 @@ +package com.fr.third.net.bytebuddy.agent; + +import lombok.EqualsAndHashCode; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.Instrumentation; +import java.lang.management.ManagementFactory; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +/** + *
+ * The Byte Buddy agent provides a JVM {@link java.lang.instrument.Instrumentation} in order to allow Byte Buddy the + * redefinition of already loaded classes. An agent must normally be specified via the command line via the + * {@code javaagent} parameter. As an argument to this parameter, one must specify the location of this agent's jar + * file such as for example in + *
+ *
+ *
+ * java -javaagent:byte-buddy-agent.jar -jar app.jar
+ *
+ *
+ * Note: The runtime installation of a Java agent is not possible on all JVMs. See the documentation for + * {@link ByteBuddyAgent#install()} for details on JVMs that are supported out of the box. + *
+ *+ * Important: This class's name is known to the Byte Buddy main application and must not be altered. + *
+ *+ * Note: Byte Buddy does not execute code using an {@link java.security.AccessController}. If a security manager + * is present, the user of this class is responsible for assuring any required privileges. + *
+ */ +public class ByteBuddyAgent { + + /** + * The manifest property specifying the agent class. + */ + private static final String AGENT_CLASS_PROPERTY = "Agent-Class"; + + /** + * The manifest property specifying the can redefine property. + */ + private static final String CAN_REDEFINE_CLASSES_PROPERTY = "Can-Redefine-Classes"; + + /** + * The manifest property specifying the can retransform property. + */ + private static final String CAN_RETRANSFORM_CLASSES_PROPERTY = "Can-Retransform-Classes"; + + /** + * The manifest property specifying the can set native method prefix property. + */ + private static final String CAN_SET_NATIVE_METHOD_PREFIX = "Can-Set-Native-Method-Prefix"; + + /** + * The manifest property value for the manifest version. + */ + private static final String MANIFEST_VERSION_VALUE = "1.0"; + + /** + * The size of the buffer for copying the agent installer file into another jar. + */ + private static final int BUFFER_SIZE = 1024; + + /** + * Convenience indices for reading and writing to the buffer to make the code more readable. + */ + private static final int START_INDEX = 0, END_OF_FILE = -1; + + /** + * The status code expected as a result of a successful attachment. + */ + private static final int SUCCESSFUL_ATTACH = 0; + + /** + * Base for access to a reflective member to make the code more readable. + */ + private static final Object STATIC_MEMBER = null; + + /** + * Representation of the bootstrap {@link java.lang.ClassLoader}. + */ + private static final ClassLoader BOOTSTRAP_CLASS_LOADER = null; + + /** + * Represents a no-op argument for a dynamic agent attachment. + */ + private static final String WITHOUT_ARGUMENT = null; + + /** + * The naming prefix of all artifacts for an attacher jar. + */ + private static final String ATTACHER_FILE_NAME = "byteBuddyAttacher"; + + /** + * The file extension for a class file. + */ + private static final String CLASS_FILE_EXTENSION = ".class"; + + /** + * The file extension for a jar file. + */ + private static final String JAR_FILE_EXTENSION = ".jar"; + + /** + * The class path argument to specify the class path elements. + */ + private static final String CLASS_PATH_ARGUMENT = "-cp"; + + /** + * The Java property denoting the Java home directory. + */ + private static final String JAVA_HOME = "java.home"; + + /** + * The Java property denoting the operating system name. + */ + private static final String OS_NAME = "os.name"; + + /** + * The name of the method for reading the installer's instrumentation. + */ + private static final String INSTRUMENTATION_METHOD = "getInstrumentation"; + + /** + * An indicator variable to express that no instrumentation is available. + */ + private static final Instrumentation UNAVAILABLE = null; + + /** + * The attachment type evaluator to be used for determining if an attachment requires an external process. + */ + private static final AttachmentTypeEvaluator ATTACHMENT_TYPE_EVALUATOR = AccessController.doPrivileged(AttachmentTypeEvaluator.InstallationAction.INSTANCE); + + /** + * The agent provides only {@code static} utility methods and should not be instantiated. + */ + private ByteBuddyAgent() { + throw new UnsupportedOperationException(); + } + + /** + *+ * Looks up the {@link java.lang.instrument.Instrumentation} instance of an installed Byte Buddy agent. Note that + * this method implies reflective lookup and reflective invocation such that the returned value should be cached + * rather than calling this method several times. + *
+ *+ * Note: This method throws an {@link java.lang.IllegalStateException} If the Byte Buddy agent is not + * properly installed. + *
+ * + * @return The {@link java.lang.instrument.Instrumentation} instance which is provided by an installed + * Byte Buddy agent. + */ + public static Instrumentation getInstrumentation() { + Instrumentation instrumentation = doGetInstrumentation(); + if (instrumentation == null) { + throw new IllegalStateException("The Byte Buddy agent is not initialized"); + } + return instrumentation; + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. The agent is not provided an argument. + * + * @param agentJar The agent jar file. + * @param processId The target process id. + */ + public static void attach(File agentJar, String processId) { + attach(agentJar, processId, WITHOUT_ARGUMENT); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. + * + * @param agentJar The agent jar file. + * @param processId The target process id. + * @param argument The argument to provide to the agent. + */ + public static void attach(File agentJar, String processId, String argument) { + attach(agentJar, processId, argument, AttachmentProvider.DEFAULT); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. This operation blocks until the + * attachment is complete. The agent is not provided an argument. + * + * @param agentJar The agent jar file. + * @param processId The target process id. + * @param attachmentProvider The attachment provider to use. + */ + public static void attach(File agentJar, String processId, AttachmentProvider attachmentProvider) { + attach(agentJar, processId, WITHOUT_ARGUMENT, attachmentProvider); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. This operation blocks until the + * attachment is complete. + * + * @param agentJar The agent jar file. + * @param processId The target process id. + * @param argument The argument to provide to the agent. + * @param attachmentProvider The attachment provider to use. + */ + public static void attach(File agentJar, String processId, String argument, AttachmentProvider attachmentProvider) { + install(attachmentProvider, processId, argument, new AgentProvider.ForExistingAgent(agentJar)); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. The agent is not provided an argument. + * + * @param agentJar The agent jar file. + * @param processProvider A provider of the target process id. + */ + public static void attach(File agentJar, ProcessProvider processProvider) { + attach(agentJar, processProvider, WITHOUT_ARGUMENT); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. The default attachment provider + * is used for applying the attachment. This operation blocks until the attachment is complete. If the current VM does not supply + * any known form of attachment to a remote VM, an {@link IllegalStateException} is thrown. + * + * @param agentJar The agent jar file. + * @param processProvider A provider of the target process id. + * @param argument The argument to provide to the agent. + */ + public static void attach(File agentJar, ProcessProvider processProvider, String argument) { + attach(agentJar, processProvider, argument, AttachmentProvider.DEFAULT); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. This operation blocks until the + * attachment is complete. The agent is not provided an argument. + * + * @param agentJar The agent jar file. + * @param processProvider A provider of the target process id. + * @param attachmentProvider The attachment provider to use. + */ + public static void attach(File agentJar, ProcessProvider processProvider, AttachmentProvider attachmentProvider) { + attach(agentJar, processProvider, WITHOUT_ARGUMENT, attachmentProvider); + } + + /** + * Attaches the given agent Jar on the target process which must be a virtual machine process. This operation blocks until the + * attachment is complete. + * + * @param agentJar The agent jar file. + * @param processProvider A provider of the target process id. + * @param argument The argument to provide to the agent. + * @param attachmentProvider The attachment provider to use. + */ + public static void attach(File agentJar, ProcessProvider processProvider, String argument, AttachmentProvider attachmentProvider) { + install(attachmentProvider, processProvider.resolve(), argument, new AgentProvider.ForExistingAgent(agentJar)); + } + + /** + *+ * Installs an agent on the currently running Java virtual machine. Unfortunately, this does + * not always work. The runtime installation of a Java agent is supported for: + *
+ *+ * If an agent cannot be installed, an {@link IllegalStateException} is thrown. + *
+ *+ * Important: This is a rather computation-heavy operation. Therefore, this operation is + * not repeated after an agent was successfully installed for the first time. Instead, the previous + * instrumentation instance is returned. However, invoking this method requires synchronization + * such that subsequently to an installation, {@link ByteBuddyAgent#getInstrumentation()} should + * be invoked instead. + *
+ * + * @return An instrumentation instance representing the currently running JVM. + */ + public static Instrumentation install() { + return install(AttachmentProvider.DEFAULT); + } + + /** + * Installs a Java agent using the Java attach API. This API is available under different + * access routes for different JVMs and JVM versions or it might not be available at all. + * If a Java agent cannot be installed by using the supplied attachment provider, an + * {@link IllegalStateException} is thrown. The same happens if the default process provider + * cannot resolve a process id for the current VM. + * + * @param attachmentProvider The attachment provider to use for the installation. + * @return An instrumentation instance representing the currently running JVM. + */ + public static Instrumentation install(AttachmentProvider attachmentProvider) { + return install(attachmentProvider, ProcessProvider.ForCurrentVm.INSTANCE); + } + + /** + * Installs a Java agent using the Java attach API. This API is available under different + * access routes for different JVMs and JVM versions or it might not be available at all. + * If a Java agent cannot be installed by using the supplied process provider, an + * {@link IllegalStateException} is thrown. The same happens if the default attachment + * provider cannot be used. + * + * @param processProvider The provider for the current JVM's process id. + * @return An instrumentation instance representing the currently running JVM. + */ + public static Instrumentation install(ProcessProvider processProvider) { + return install(AttachmentProvider.DEFAULT, processProvider); + } + + /** + * Installs a Java agent using the Java attach API. This API is available under different + * access routes for different JVMs and JVM versions or it might not be available at all. + * If a Java agent cannot be installed by using the supplied attachment provider and process + * provider, an {@link IllegalStateException} is thrown. + * + * @param attachmentProvider The attachment provider to use for the installation. + * @param processProvider The provider for the current JVM's process id. + * @return An instrumentation instance representing the currently running JVM. + */ + public static synchronized Instrumentation install(AttachmentProvider attachmentProvider, ProcessProvider processProvider) { + Instrumentation instrumentation = doGetInstrumentation(); + if (instrumentation != null) { + return instrumentation; + } + install(attachmentProvider, processProvider.resolve(), WITHOUT_ARGUMENT, AgentProvider.ForByteBuddyAgent.INSTANCE); + return doGetInstrumentation(); + } + + /** + * Installs a Java agent on a target VM. + * + * @param attachmentProvider The attachment provider to use. + * @param processId The process id of the target JVM process. + * @param argument The argument to provide to the agent. + * @param agentProvider The agent provider for the agent jar. + */ + private static void install(AttachmentProvider attachmentProvider, String processId, String argument, AgentProvider agentProvider) { + AttachmentProvider.Accessor attachmentAccessor = attachmentProvider.attempt(); + if (!attachmentAccessor.isAvailable()) { + throw new IllegalStateException("No compatible attachment provider is not available"); + } + try { + if (ATTACHMENT_TYPE_EVALUATOR.requiresExternalAttachment(processId)) { + installExternal(attachmentAccessor.getExternalAttachment(), processId, agentProvider.resolve(), argument); + } else { + Attacher.install(attachmentAccessor.getVirtualMachineType(), processId, agentProvider.resolve().getAbsolutePath(), argument); + } + } catch (RuntimeException exception) { + throw exception; + } catch (Exception exception) { + throw new IllegalStateException("Error during attachment using: " + attachmentProvider, exception); + } + } + + /** + * Installs a Java agent to the current VM via an external process. This is typically required starting with OpenJDK 9 + * when the {@code jdk.attach.allowAttachSelf} property is set to {@code false} what is the default setting. + * + * @param externalAttachment A description of the external attachment. + * @param processId The process id of the current process. + * @param agent The Java agent to install. + * @param argument The argument to provide to the agent or {@code null} if no argument should be supplied. + * @throws Exception If an exception occurs during the attachment or the external process fails the attachment. + */ + private static void installExternal(AttachmentProvider.Accessor.ExternalAttachment externalAttachment, + String processId, + File agent, + String argument) throws Exception { + InputStream inputStream = Attacher.class.getResourceAsStream('/' + Attacher.class.getName().replace('.', '/') + CLASS_FILE_EXTENSION); + if (inputStream == null) { + throw new IllegalStateException("Cannot locate class file for Byte Buddy installation process"); + } + File attachmentJar = null; + try { + try { + attachmentJar = File.createTempFile(ATTACHER_FILE_NAME, JAR_FILE_EXTENSION); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(attachmentJar)); + try { + jarOutputStream.putNextEntry(new JarEntry(Attacher.class.getName().replace('.', '/') + CLASS_FILE_EXTENSION)); + byte[] buffer = new byte[BUFFER_SIZE]; + int index; + while ((index = inputStream.read(buffer)) != END_OF_FILE) { + jarOutputStream.write(buffer, START_INDEX, index); + } + jarOutputStream.closeEntry(); + } finally { + jarOutputStream.close(); + } + } finally { + inputStream.close(); + } + StringBuilder classPath = new StringBuilder().append(quote(attachmentJar.getCanonicalPath())); + for (File jar : externalAttachment.getClassPath()) { + classPath.append(File.pathSeparatorChar).append(quote(jar.getCanonicalPath())); + } + if (new ProcessBuilder(quote(System.getProperty(JAVA_HOME) + + File.separatorChar + "bin" + + File.separatorChar + (System.getProperty(OS_NAME, "").toLowerCase(Locale.US).contains("windows") ? "java.exe" : "java")), + CLASS_PATH_ARGUMENT, + classPath.toString(), + Attacher.class.getName(), + externalAttachment.getVirtualMachineType(), + processId, + quote(agent.getAbsolutePath()), + argument == null ? "" : ("=" + argument)).start().waitFor() != SUCCESSFUL_ATTACH) { + throw new IllegalStateException("Could not self-attach to current VM using external process"); + } + } finally { + if (attachmentJar != null) { + if (!attachmentJar.delete()) { + attachmentJar.deleteOnExit(); + } + } + } + } + + /** + * Quotes a value if it contains a white space. + * + * @param value The value to quote. + * @return The value being quoted if necessary. + */ + private static String quote(String value) { + return value.contains(" ") + ? '"' + value + '"' + : value; + } + + /** + * Performs the actual lookup of the {@link java.lang.instrument.Instrumentation} from an installed + * Byte Buddy agent. + * + * @return The Byte Buddy agent's {@link java.lang.instrument.Instrumentation} instance. + */ + private static Instrumentation doGetInstrumentation() { + try { + return (Instrumentation) ClassLoader.getSystemClassLoader() + .loadClass(Installer.class.getName()) + .getMethod(INSTRUMENTATION_METHOD) + .invoke(STATIC_MEMBER); + } catch (Exception ignored) { + return UNAVAILABLE; + } + } + + /** + * An attachment provider is responsible for making the Java attachment API available. + */ + public interface AttachmentProvider { + + /** + * The default attachment provider to be used. + */ + AttachmentProvider DEFAULT = new Compound(ForJigsawVm.INSTANCE, + ForJ9Vm.INSTANCE, + ForToolsJarVm.JVM_ROOT, + ForToolsJarVm.JDK_ROOT, + ForToolsJarVm.MACINTOSH, + ForUnixHotSpotVm.INSTANCE); + + /** + * Attempts the creation of an accessor for a specific JVM's attachment API. + * + * @return The accessor this attachment provider can supply for the currently running JVM. + */ + Accessor attempt(); + + /** + * An accessor for a JVM's attachment API. + */ + interface Accessor { + + /** + * The name of the {@code VirtualMachine} class on any OpenJDK or Oracle JDK implementation. + */ + String VIRTUAL_MACHINE_TYPE_NAME = "com.sun.tools.attach.VirtualMachine"; + + /** + * The name of the {@code VirtualMachine} class on IBM J9 VMs. + */ + String VIRTUAL_MACHINE_TYPE_NAME_J9 = "com.ibm.tools.attach.VirtualMachine"; + + /** + * Determines if this accessor is applicable for the currently running JVM. + * + * @return {@code true} if this accessor is available. + */ + boolean isAvailable(); + + /** + * Returns a {@code VirtualMachine} class. This method must only be called for available accessors. + * + * @return The virtual machine type. + */ + Class> getVirtualMachineType(); + + /** + * Returns a description of a virtual machine class for an external attachment. + * + * @return A description of the external attachment. + */ + ExternalAttachment getExternalAttachment(); + + /** + * A canonical implementation of an unavailable accessor. + */ + enum Unavailable implements Accessor { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public boolean isAvailable() { + return false; + } + + @Override + public Class> getVirtualMachineType() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + + @Override + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + } + + /** + * Describes an external attachment to a Java virtual machine. + */ + @EqualsAndHashCode + class ExternalAttachment { + + /** + * The fully-qualified binary name of the virtual machine type. + */ + private final String virtualMachineType; + + /** + * The class path elements required for loading the supplied virtual machine type. + */ + private final List+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.sun.tools.attach.VirtualMachine} class from the provided class loader. + *
+ *+ * This accessor is supposed to work on any implementation of the OpenJDK or Oracle JDK. + *
+ * + * @param classLoader A class loader that is capable of loading the virtual machine type. + * @param classPath The class path required to load the virtual machine class. + * @return An appropriate accessor. + */ + public static Accessor of(ClassLoader classLoader, File... classPath) { + try { + return new Simple.WithExternalAttachment(classLoader.loadClass(VIRTUAL_MACHINE_TYPE_NAME), + Arrays.asList(classPath)); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + /** + *+ * Creates an accessor by reading the process id from the JMX runtime bean and by attempting + * to load the {@code com.ibm.tools.attach.VirtualMachine} class from the provided class loader. + *
+ *+ * This accessor is supposed to work on any implementation of IBM's J9. + *
+ * + * @return An appropriate accessor. + */ + public static Accessor ofJ9() { + try { + return new Simple.WithExternalAttachment(ClassLoader.getSystemClassLoader().loadClass(VIRTUAL_MACHINE_TYPE_NAME_J9), + Collections.+ * Returns the instrumentation that was loaded by the Byte Buddy agent. When a security manager is active, + * the {@link RuntimePermission} for {@code getInstrumentation} is required by the caller. + *
+ *+ * Important: This method must only be invoked via the {@link ClassLoader#getSystemClassLoader()} where any + * Java agent is loaded. It is possible that two versions of this class exist for different class loaders. + *
+ * + * @return The instrumentation instance of the Byte Buddy agent. + */ + public static Instrumentation getInstrumentation() { + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) { + securityManager.checkPermission(new RuntimePermission("getInstrumentation")); + } + Instrumentation instrumentation = Installer.instrumentation; + if (instrumentation == null) { + throw new IllegalStateException("The Byte Buddy agent is not loaded or this method is not called via the system class loader"); + } + return instrumentation; + } + + /** + * Allows the installation of this agent via a command line argument. + * + * @param agentArguments The unused agent arguments. + * @param instrumentation The instrumentation instance. + */ + public static void premain(String agentArguments, Instrumentation instrumentation) { + Installer.instrumentation = instrumentation; + } + + /** + * Allows the installation of this agent via the Attach API. + * + * @param agentArguments The unused agent arguments. + * @param instrumentation The instrumentation instance. + */ + @SuppressWarnings("unused") + public static void agentmain(String agentArguments, Instrumentation instrumentation) { + Installer.instrumentation = instrumentation; + } +} diff --git a/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/VirtualMachine.java b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/VirtualMachine.java new file mode 100644 index 000000000..4409cb3e1 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/VirtualMachine.java @@ -0,0 +1,336 @@ +package com.fr.third.net.bytebuddy.agent; + +import com.fr.third.org.newsclub.net.unix.AFUNIXSocket; +import com.fr.third.org.newsclub.net.unix.AFUNIXSocketAddress; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +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 {@link 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 {@code attach(String)} returning an + * instance of a class that declares the methods defined by {@link VirtualMachine}. + *
+ */ +public interface VirtualMachine { + + /** + * 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. + */ + @SuppressWarnings("unused") + void detach() throws IOException; + + /** + * A virtual machine implementation for a HotSpot VM or any compatible VM. + */ + abstract class ForHotSpot implements VirtualMachine { + + /** + * The UTF-8 charset. + */ + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + /** + * The protocol version to use for communication. + */ + private static final String PROTOCOL_VERSION = "1"; + + /** + * The {@code load} command. + */ + private static final String LOAD_COMMAND = "load"; + + /** + * The {@code instrument} command. + */ + private static final String INSTRUMENT_COMMAND = "instrument"; + + /** + * A delimiter to be used for attachment. + */ + private static final String ARGUMENT_DELIMITER = "="; + + /** + * A blank line argument. + */ + private static final byte[] BLANK = new byte[]{0}; + + /** + * The target process's id. + */ + protected final String processId; + + /** + * Creates a new HotSpot-compatible VM implementation. + * + * @param processId The target process's id. + */ + protected ForHotSpot(String processId) { + this.processId = processId; + } + + @Override + 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. + */ + protected abstract void connect() throws IOException; + + /** + * 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. + * + * @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. + */ + public static class OnUnix extends ForHotSpot { + + /** + * The default amount of attempts to connect. + */ + private static final int DEFAULT_ATTEMPTS = 10; + + /** + * The default pause between two attempts. + */ + private static final long DEFAULT_PAUSE = 200; + + /** + * The default socket timeout. + */ + private static final long DEFAULT_TIMEOUT = 5000; + + /** + * The temporary directory on Unix systems. + */ + private static final String TEMPORARY_DIRECTORY = "/tmp"; + + /** + * The name prefix for a socket. + */ + private static final String SOCKET_FILE_PREFIX = ".java_pid"; + + /** + * 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 {@link AFUNIXSocket} which is however not set to avoid eager loading + */ + private final Object socket; + + /** + * The number of attempts to connect. + */ + private final int attempts; + + /** + * The time to pause between attempts. + */ + private final long pause; + + /** + * The socket timeout. + */ + private final long timeout; + + /** + * The time unit of the pause time. + */ + private final TimeUnit timeUnit; + + /** + * 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 {@link Throwable} is thrown. + * + * @return This virtual machine type. + * @throws Throwable If this VM does not support POSIX sockets or is not running on a HotSpot VM. + */ + public static Class> assertAvailability() throws Throwable { + 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. + * + * @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); + } + + @Override + 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(); + } + } + } + ((AFUNIXSocket) socket).setSoTimeout((int) timeUnit.toMillis(timeout)); + ((AFUNIXSocket) socket).connect(new AFUNIXSocketAddress(socketFile)); + } + + @Override + public int read(byte[] buffer) throws IOException { + return ((AFUNIXSocket) this.socket).getInputStream().read(buffer); + } + + @Override + public void write(byte[] buffer) throws IOException { + ((AFUNIXSocket) this.socket).getOutputStream().write(buffer); + } + + @Override + public void detach() throws IOException { + ((AFUNIXSocket) this.socket).close(); + } + } + } +} diff --git a/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/package-info.java b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/package-info.java new file mode 100644 index 000000000..c31de204f --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/package-info.java @@ -0,0 +1,4 @@ +/** + * The Byte Buddy agent allows the redefinition of classes at runtime. + */ +package com.fr.third.net.bytebuddy.agent; diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXServerSocket.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXServerSocket.java new file mode 100644 index 000000000..549f97df3 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXServerSocket.java @@ -0,0 +1,141 @@ +/** + * junixsocket + * + * Copyright (c) 2009,2014 Christian Kohlschütter + * + * The author 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 com.fr.third.org.newsclub.net.unix; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; + +/** + * The server part of an AF_UNIX domain socket. + * + * @author Christian Kohlschütter + */ +public class AFUNIXServerSocket extends ServerSocket { + private final AFUNIXSocketImpl implementation; + private AFUNIXSocketAddress boundEndpoint = null; + + private final Thread shutdownThread = new Thread() { + @Override + public void run() { + try { + if (boundEndpoint != null) { + NativeUnixSocket.unlink(boundEndpoint.getSocketFile()); + } + } catch (IOException e) { + // ignore + } + } + }; + + protected AFUNIXServerSocket() throws IOException { + super(); + this.implementation = new AFUNIXSocketImpl(); + NativeUnixSocket.initServerImpl(this, implementation); + + Runtime.getRuntime().addShutdownHook(shutdownThread); + NativeUnixSocket.setCreatedServer(this); + } + + /** + * Returns a new, unbound AF_UNIX {@link ServerSocket}. + * + * @return The new, unbound {@link AFUNIXServerSocket}. + */ + public static AFUNIXServerSocket newInstance() throws IOException { + AFUNIXServerSocket instance = new AFUNIXServerSocket(); + return instance; + } + + /** + * Returns a new AF_UNIX {@link ServerSocket} that is bound to the given + * {@link AFUNIXSocketAddress}. + * + * @return The new, unbound {@link AFUNIXServerSocket}. + */ + public static AFUNIXServerSocket bindOn(final AFUNIXSocketAddress addr) throws IOException { + AFUNIXServerSocket socket = newInstance(); + socket.bind(addr); + return socket; + } + + @Override + public void bind(SocketAddress endpoint, int backlog) throws IOException { + if (isClosed()) { + throw new SocketException("Socket is closed"); + } + if (isBound()) { + throw new SocketException("Already bound"); + } + if (!(endpoint instanceof AFUNIXSocketAddress)) { + throw new IOException("Can only bind to endpoints of type " + + AFUNIXSocketAddress.class.getName()); + } + implementation.bind(backlog, endpoint); + boundEndpoint = (AFUNIXSocketAddress) endpoint; + } + + @Override + public boolean isBound() { + return boundEndpoint != null; + } + + @Override + public Socket accept() throws IOException { + if (isClosed()) { + throw new SocketException("Socket is closed"); + } + AFUNIXSocket as = AFUNIXSocket.newInstance(); + implementation.accept(as.impl); + as.addr = boundEndpoint; + NativeUnixSocket.setConnected(as); + return as; + } + + @Override + public String toString() { + if (!isBound()) { + return "AFUNIXServerSocket[unbound]"; + } + return "AFUNIXServerSocket[" + boundEndpoint.getSocketFile() + "]"; + } + + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + + super.close(); + implementation.close(); + if (boundEndpoint != null) { + NativeUnixSocket.unlink(boundEndpoint.getSocketFile()); + } + try { + Runtime.getRuntime().removeShutdownHook(shutdownThread); + } catch (IllegalStateException e) { + // ignore + } + } + + public static boolean isSupported() { + return NativeUnixSocket.isLoaded(); + } +} diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocket.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocket.java new file mode 100644 index 000000000..815ca01f8 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocket.java @@ -0,0 +1,131 @@ +/** + * junixsocket + * + * Copyright (c) 2009,2014 Christian Kohlschütter + * + * The author 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 com.fr.third.org.newsclub.net.unix; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; + +/** + * Implementation of an AF_UNIX domain socket. + * + * @author Christian Kohlschütter + */ +public class AFUNIXSocket extends Socket { + protected AFUNIXSocketImpl impl; + AFUNIXSocketAddress addr; + + private AFUNIXSocket(final AFUNIXSocketImpl impl) throws IOException { + super(impl); + try { + NativeUnixSocket.setCreated(this); + } catch (UnsatisfiedLinkError e) { + e.printStackTrace(); + } + } + + /** + * Creates a new, unbound {@link AFUNIXSocket}. + * + * This "default" implementation is a bit "lenient" with respect to the specification. + * + * In particular, we ignore calls to {@link Socket#getTcpNoDelay()} and + * {@link Socket#setTcpNoDelay(boolean)}. + * + * @return A new, unbound socket. + */ + public static AFUNIXSocket newInstance() throws IOException { + final AFUNIXSocketImpl impl = new AFUNIXSocketImpl.Lenient(); + AFUNIXSocket instance = new AFUNIXSocket(impl); + instance.impl = impl; + return instance; + } + + /** + * Creates a new, unbound, "strict" {@link AFUNIXSocket}. + * + * This call uses an implementation that tries to be closer to the specification than + * {@link #newInstance()}, at least for some cases. + * + * @return A new, unbound socket. + */ + public static AFUNIXSocket newStrictInstance() throws IOException { + final AFUNIXSocketImpl impl = new AFUNIXSocketImpl(); + AFUNIXSocket instance = new AFUNIXSocket(impl); + instance.impl = impl; + return instance; + } + + /** + * Creates a new {@link AFUNIXSocket} and connects it to the given {@link AFUNIXSocketAddress}. + * + * @param addr The address to connect to. + * @return A new, connected socket. + */ + public static AFUNIXSocket connectTo(AFUNIXSocketAddress addr) throws IOException { + AFUNIXSocket socket = newInstance(); + socket.connect(addr); + return socket; + } + + /** + * Binds this {@link AFUNIXSocket} to the given bindpoint. Only bindpoints of the type + * {@link AFUNIXSocketAddress} are supported. + */ + @Override + public void bind(SocketAddress bindpoint) throws IOException { + super.bind(bindpoint); + this.addr = (AFUNIXSocketAddress) bindpoint; + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + connect(endpoint, 0); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + if (!(endpoint instanceof AFUNIXSocketAddress)) { + throw new IOException("Can only connect to endpoints of type " + + AFUNIXSocketAddress.class.getName()); + } + impl.connect(endpoint, timeout); + this.addr = (AFUNIXSocketAddress) endpoint; + NativeUnixSocket.setConnected(this); + } + + @Override + public String toString() { + if (isConnected()) { + return "AFUNIXSocket[fd=" + impl.getFD() + ";path=" + addr.getSocketFile() + "]"; + } + return "AFUNIXSocket[unconnected]"; + } + + /** + * Returnstrue
iff {@link AFUNIXSocket}s are supported by the current Java VM.
+ *
+ * To support {@link AFUNIXSocket}s, a custom JNI library must be loaded that is supplied with
+ * junixsocket.
+ *
+ * @return {@code true} iff supported.
+ */
+ public static boolean isSupported() {
+ return NativeUnixSocket.isLoaded();
+ }
+}
diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketAddress.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketAddress.java
new file mode 100644
index 000000000..17678fa2a
--- /dev/null
+++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketAddress.java
@@ -0,0 +1,76 @@
+/**
+ * junixsocket
+ *
+ * Copyright (c) 2009,2014 Christian Kohlschütter
+ *
+ * The author 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 com.fr.third.org.newsclub.net.unix;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+/**
+ * Describes an {@link InetSocketAddress} that actually uses AF_UNIX sockets instead of AF_INET.
+ *
+ * The ability to specify a port number is not specified by AF_UNIX sockets, but we need it
+ * sometimes, for example for RMI-over-AF_UNIX.
+ *
+ * @author Christian Kohlschütter
+ */
+public class AFUNIXSocketAddress extends InetSocketAddress {
+
+ private static final long serialVersionUID = 1L;
+ private final String socketFile;
+
+ /**
+ * Creates a new {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the
+ * given file.
+ *
+ * @param socketFile The socket to connect to.
+ */
+ public AFUNIXSocketAddress(final File socketFile) throws IOException {
+ this(socketFile, 0);
+ }
+
+ /**
+ * Creates a new {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the
+ * given file, assigning the given port to it.
+ *
+ * @param socketFile The socket to connect to.
+ * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
+ */
+ public AFUNIXSocketAddress(final File socketFile, int port) throws IOException {
+ super(0);
+ if (port != 0) {
+ NativeUnixSocket.setPort1(this, port);
+ }
+ this.socketFile = socketFile.getCanonicalPath();
+ }
+
+ /**
+ * Returns the (canonical) file path for this {@link AFUNIXSocketAddress}.
+ *
+ * @return The file path.
+ */
+ public String getSocketFile() {
+ return socketFile;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getName() + "[host=" + getHostName() + ";port=" + getPort() + ";file="
+ + socketFile + "]";
+ }
+}
diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketException.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketException.java
new file mode 100644
index 000000000..54778435c
--- /dev/null
+++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketException.java
@@ -0,0 +1,53 @@
+/**
+ * junixsocket
+ *
+ * Copyright (c) 2009,2014 Christian Kohlschütter
+ *
+ * The author 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 com.fr.third.org.newsclub.net.unix;
+
+import java.net.SocketException;
+
+/**
+ * Something went wrong with the communication to a Unix socket.
+ *
+ * @author Christian Kohlschütter
+ */
+public class AFUNIXSocketException extends SocketException {
+ private static final long serialVersionUID = 1L;
+ private final String socketFile;
+
+ public AFUNIXSocketException(String reason) {
+ this(reason, (String) null);
+ }
+
+ public AFUNIXSocketException(String reason, final Throwable cause) {
+ this(reason, (String) null);
+ initCause(cause);
+ }
+
+ public AFUNIXSocketException(String reason, final String socketFile) {
+ super(reason);
+ this.socketFile = socketFile;
+ }
+
+ @Override
+ public String toString() {
+ if (socketFile == null) {
+ return super.toString();
+ } else {
+ return super.toString() + " (socket: " + socketFile + ")";
+ }
+ }
+}
diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketImpl.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketImpl.java
new file mode 100644
index 000000000..757d32039
--- /dev/null
+++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/AFUNIXSocketImpl.java
@@ -0,0 +1,411 @@
+/**
+ * junixsocket
+ *
+ * Copyright (c) 2009,2014 Christian Kohlschütter
+ *
+ * The author 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 com.fr.third.org.newsclub.net.unix;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.SocketImpl;
+import java.net.SocketOptions;
+
+/**
+ * The Java-part of the {@link AFUNIXSocket} implementation.
+ *
+ * @author Christian Kohlschütter
+ */
+class AFUNIXSocketImpl extends SocketImpl {
+ private static final int SHUT_RD = 0;
+ private static final int SHUT_WR = 1;
+ private static final int SHUT_RD_WR = 2;
+
+ private String socketFile;
+ private boolean closed = false;
+ private boolean bound = false;
+ private boolean connected = false;
+
+ private boolean closedInputStream = false;
+ private boolean closedOutputStream = false;
+
+ private final AFUNIXInputStream in = new AFUNIXInputStream();
+ private final AFUNIXOutputStream out = new AFUNIXOutputStream();
+
+ AFUNIXSocketImpl() {
+ super();
+ this.fd = new FileDescriptor();
+ }
+
+ FileDescriptor getFD() {
+ return fd;
+ }
+
+ @Override
+ protected void accept(SocketImpl socket) throws IOException {
+ final AFUNIXSocketImpl si = (AFUNIXSocketImpl) socket;
+ NativeUnixSocket.accept(socketFile, fd, si.fd);
+ si.socketFile = socketFile;
+ si.connected = true;
+ }
+
+ @Override
+ protected int available() throws IOException {
+ return NativeUnixSocket.available(fd);
+ }
+
+ protected void bind(SocketAddress addr) throws IOException {
+ bind(0, addr);
+ }
+
+ protected void bind(int backlog, SocketAddress addr) throws IOException {
+ if (!(addr instanceof AFUNIXSocketAddress)) {
+ throw new SocketException("Cannot bind to this type of address: " + addr.getClass());
+ }
+ final AFUNIXSocketAddress socketAddress = (AFUNIXSocketAddress) addr;
+ socketFile = socketAddress.getSocketFile();
+ NativeUnixSocket.bind(socketFile, fd, backlog);
+ bound = true;
+ this.localport = socketAddress.getPort();
+ }
+
+ @Override
+ @SuppressWarnings("hiding")
+ protected void bind(InetAddress host, int port) throws IOException {
+ throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
+ }
+
+ private void checkClose() throws IOException {
+ if (closedInputStream && closedOutputStream) {
+ // close();
+ }
+ }
+
+ @Override
+ protected synchronized void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ if (fd.valid()) {
+ NativeUnixSocket.shutdown(fd, SHUT_RD_WR);
+ NativeUnixSocket.close(fd);
+ }
+ if (bound) {
+ NativeUnixSocket.unlink(socketFile);
+ }
+ connected = false;
+ }
+
+ @Override
+ @SuppressWarnings("hiding")
+ protected void connect(String host, int port) throws IOException {
+ throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
+ }
+
+ @Override
+ @SuppressWarnings("hiding")
+ protected void connect(InetAddress address, int port) throws IOException {
+ throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
+ }
+
+ @Override
+ protected void connect(SocketAddress addr, int timeout) throws IOException {
+ if (!(addr instanceof AFUNIXSocketAddress)) {
+ throw new SocketException("Cannot bind to this type of address: " + addr.getClass());
+ }
+ final AFUNIXSocketAddress socketAddress = (AFUNIXSocketAddress) addr;
+ socketFile = socketAddress.getSocketFile();
+ NativeUnixSocket.connect(socketFile, fd);
+ this.address = socketAddress.getAddress();
+ this.port = socketAddress.getPort();
+ this.localport = 0;
+ this.connected = true;
+ }
+
+ @Override
+ protected void create(boolean stream) throws IOException {
+ }
+
+ @Override
+ protected InputStream getInputStream() throws IOException {
+ if (!connected && !bound) {
+ throw new IOException("Not connected/not bound");
+ }
+ return in;
+ }
+
+ @Override
+ protected OutputStream getOutputStream() throws IOException {
+ if (!connected && !bound) {
+ throw new IOException("Not connected/not bound");
+ }
+ return out;
+ }
+
+ @Override
+ protected void listen(int backlog) throws IOException {
+ NativeUnixSocket.listen(fd, backlog);
+ }
+
+ @Override
+ protected void sendUrgentData(int data) throws IOException {
+ NativeUnixSocket.write(fd, new byte[] {(byte) (data & 0xFF)}, 0, 1);
+ }
+
+ private final class AFUNIXInputStream extends InputStream {
+ private boolean streamClosed = false;
+
+ @Override
+ public int read(byte[] buf, int off, int len) throws IOException {
+ if (streamClosed) {
+ throw new IOException("This InputStream has already been closed.");
+ }
+ if (len == 0) {
+ return 0;
+ }
+ int maxRead = buf.length - off;
+ if (len > maxRead) {
+ len = maxRead;
+ }
+ try {
+ return NativeUnixSocket.read(fd, buf, off, len);
+ } catch (final IOException e) {
+ throw (IOException) new IOException(e.getMessage() + " at "
+ + AFUNIXSocketImpl.this.toString()).initCause(e);
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ final byte[] buf1 = new byte[1];
+ final int numRead = read(buf1, 0, 1);
+ if (numRead <= 0) {
+ return -1;
+ } else {
+ return buf1[0] & 0xFF;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (streamClosed) {
+ return;
+ }
+ streamClosed = true;
+ if (fd.valid()) {
+ NativeUnixSocket.shutdown(fd, SHUT_RD);
+ }
+
+ closedInputStream = true;
+ checkClose();
+ }
+
+ @Override
+ public int available() throws IOException {
+ final int av = NativeUnixSocket.available(fd);
+ return av;
+ }
+ }
+
+ private final class AFUNIXOutputStream extends OutputStream {
+ private boolean streamClosed = false;
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ final byte[] buf1 = new byte[] {(byte) oneByte};
+ write(buf1, 0, 1);
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) throws IOException {
+ if (streamClosed) {
+ throw new AFUNIXSocketException("This OutputStream has already been closed.");
+ }
+ if (len > buf.length - off) {
+ throw new IndexOutOfBoundsException();
+ }
+ try {
+ while (len > 0 && !Thread.interrupted()) {
+ final int written = NativeUnixSocket.write(fd, buf, off, len);
+ if (written == -1) {
+ throw new IOException("Unspecific error while writing");
+ }
+ len -= written;
+ off += written;
+ }
+ } catch (final IOException e) {
+ throw (IOException) new IOException(e.getMessage() + " at "
+ + AFUNIXSocketImpl.this.toString()).initCause(e);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (streamClosed) {
+ return;
+ }
+ streamClosed = true;
+ if (fd.valid()) {
+ NativeUnixSocket.shutdown(fd, SHUT_WR);
+ }
+ closedOutputStream = true;
+ checkClose();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "[fd=" + fd + "; file=" + this.socketFile + "; connected="
+ + connected + "; bound=" + bound + "]";
+ }
+
+ private static int expectInteger(Object value) throws SocketException {
+ try {
+ return (Integer) value;
+ } catch (final ClassCastException e) {
+ throw new AFUNIXSocketException("Unsupported value: " + value, e);
+ } catch (final NullPointerException e) {
+ throw new AFUNIXSocketException("Value must not be null", e);
+ }
+ }
+
+ private static int expectBoolean(Object value) throws SocketException {
+ try {
+ return ((Boolean) value).booleanValue() ? 1 : 0;
+ } catch (final ClassCastException e) {
+ throw new AFUNIXSocketException("Unsupported value: " + value, e);
+ } catch (final NullPointerException e) {
+ throw new AFUNIXSocketException("Value must not be null", e);
+ }
+ }
+
+ @Override
+ public Object getOption(int optID) throws SocketException {
+ try {
+ switch (optID) {
+ case SocketOptions.SO_KEEPALIVE:
+ case SocketOptions.TCP_NODELAY:
+ return NativeUnixSocket.getSocketOptionInt(fd, optID) != 0 ? true : false;
+ case SocketOptions.SO_LINGER:
+ case SocketOptions.SO_TIMEOUT:
+ case SocketOptions.SO_RCVBUF:
+ case SocketOptions.SO_SNDBUF:
+ return NativeUnixSocket.getSocketOptionInt(fd, optID);
+ default:
+ throw new AFUNIXSocketException("Unsupported option: " + optID);
+ }
+ } catch (final AFUNIXSocketException e) {
+ throw e;
+ } catch (final Exception e) {
+ throw new AFUNIXSocketException("Error while getting option", e);
+ }
+ }
+
+ @Override
+ public void setOption(int optID, Object value) throws SocketException {
+ try {
+ switch (optID) {
+ case SocketOptions.SO_LINGER:
+
+ if (value instanceof Boolean) {
+ final boolean b = (Boolean) value;
+ if (b) {
+ throw new SocketException("Only accepting Boolean.FALSE here");
+ }
+ NativeUnixSocket.setSocketOptionInt(fd, optID, -1);
+ return;
+ }
+ NativeUnixSocket.setSocketOptionInt(fd, optID, expectInteger(value));
+ return;
+ case SocketOptions.SO_RCVBUF:
+ case SocketOptions.SO_SNDBUF:
+ case SocketOptions.SO_TIMEOUT:
+ NativeUnixSocket.setSocketOptionInt(fd, optID, expectInteger(value));
+ return;
+ case SocketOptions.SO_KEEPALIVE:
+ case SocketOptions.TCP_NODELAY:
+ NativeUnixSocket.setSocketOptionInt(fd, optID, expectBoolean(value));
+ return;
+ default:
+ throw new AFUNIXSocketException("Unsupported option: " + optID);
+ }
+ } catch (final AFUNIXSocketException e) {
+ throw e;
+ } catch (final Exception e) {
+ throw new AFUNIXSocketException("Error while setting option", e);
+ }
+ }
+
+ @Override
+ protected void shutdownInput() throws IOException {
+ if (!closed && fd.valid()) {
+ NativeUnixSocket.shutdown(fd, SHUT_RD);
+ }
+ }
+
+ @Override
+ protected void shutdownOutput() throws IOException {
+ if (!closed && fd.valid()) {
+ NativeUnixSocket.shutdown(fd, SHUT_WR);
+ }
+ }
+
+ /**
+ * Changes the behavior to be somewhat lenient with respect to the specification.
+ *
+ * In particular, we ignore calls to {@link Socket#getTcpNoDelay()} and
+ * {@link Socket#setTcpNoDelay(boolean)}.
+ */
+ static class Lenient extends AFUNIXSocketImpl {
+ Lenient() {
+ super();
+ }
+
+ @Override
+ public void setOption(int optID, Object value) throws SocketException {
+ try {
+ super.setOption(optID, value);
+ } catch (SocketException e) {
+ switch (optID) {
+ case SocketOptions.TCP_NODELAY:
+ return;
+ default:
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public Object getOption(int optID) throws SocketException {
+ try {
+ return super.getOption(optID);
+ } catch (SocketException e) {
+ switch (optID) {
+ case SocketOptions.TCP_NODELAY:
+ case SocketOptions.SO_KEEPALIVE:
+ return false;
+ default:
+ throw e;
+ }
+ }
+ }
+ }
+}
diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/NativeUnixSocket.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/NativeUnixSocket.java
new file mode 100644
index 000000000..3ff6b5cbe
--- /dev/null
+++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/NativeUnixSocket.java
@@ -0,0 +1,131 @@
+/**
+ * junixsocket
+ *
+ * Copyright (c) 2009,2014 Christian Kohlschütter
+ *
+ * The author 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 com.fr.third.org.newsclub.net.unix;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+
+/**
+ * JNI connector to native JNI C code.
+ *
+ * @author Christian Kohlschütter
+ */
+final class NativeUnixSocket {
+ private static boolean loaded = false;
+
+ static {
+ try {
+ Class.forName("org.newsclub.net.unix.NarSystem").getMethod("loadLibrary").invoke(null);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(
+ "Could not find NarSystem class.\n\n*** ECLIPSE USERS ***\nIf you're running from "
+ + "within Eclipse, please try closing the \"junixsocket-native-common\" "
+ + "project\n", e);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ loaded = true;
+ }
+
+ static boolean isLoaded() {
+ return loaded;
+ }
+
+ static void checkSupported() {
+ }
+
+ static native void bind(final String socketFile, final FileDescriptor fd, final int backlog)
+ throws IOException;
+
+ static native void listen(final FileDescriptor fd, final int backlog) throws IOException;
+
+ static native void accept(final String socketFile, final FileDescriptor fdServer,
+ final FileDescriptor fd) throws IOException;
+
+ static native void connect(final String socketFile, final FileDescriptor fd) throws IOException;
+
+ static native int read(final FileDescriptor fd, byte[] buf, int off, int len) throws IOException;
+
+ static native int write(final FileDescriptor fd, byte[] buf, int off, int len) throws IOException;
+
+ static native void close(final FileDescriptor fd) throws IOException;
+
+ static native void shutdown(final FileDescriptor fd, int mode) throws IOException;
+
+ static native int getSocketOptionInt(final FileDescriptor fd, int optionId) throws IOException;
+
+ static native void setSocketOptionInt(final FileDescriptor fd, int optionId, int value)
+ throws IOException;
+
+ static native void unlink(final String socketFile) throws IOException;
+
+ static native int available(final FileDescriptor fd) throws IOException;
+
+ static native void initServerImpl(final AFUNIXServerSocket serverSocket,
+ final AFUNIXSocketImpl impl);
+
+ static native void setCreated(final AFUNIXSocket socket);
+
+ static native void setConnected(final AFUNIXSocket socket);
+
+ static native void setBound(final AFUNIXSocket socket);
+
+ static native void setCreatedServer(final AFUNIXServerSocket socket);
+
+ static native void setBoundServer(final AFUNIXServerSocket socket);
+
+ static native void setPort(final AFUNIXSocketAddress addr, int port);
+
+ static void setPort1(AFUNIXSocketAddress addr, int port) throws AFUNIXSocketException {
+ if (port < 0) {
+ throw new IllegalArgumentException("port out of range:" + port);
+ }
+
+ boolean setOk = false;
+ try {
+ final Field holderField = InetSocketAddress.class.getDeclaredField("holder");
+ if (holderField != null) {
+ holderField.setAccessible(true);
+
+ final Object holder = holderField.get(addr);
+ if (holder != null) {
+ final Field portField = holder.getClass().getDeclaredField("port");
+ if (portField != null) {
+ portField.setAccessible(true);
+ portField.set(holder, port);
+ setOk = true;
+ }
+ }
+ } else {
+ setPort(addr, port);
+ }
+ } catch (final RuntimeException e) {
+ throw e;
+ } catch (final Exception e) {
+ if (e instanceof AFUNIXSocketException) {
+ throw (AFUNIXSocketException) e;
+ }
+ throw new AFUNIXSocketException("Could not set port", e);
+ }
+ if (!setOk) {
+ throw new AFUNIXSocketException("Could not set port");
+ }
+ }
+}
diff --git a/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/package-info.java b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/package-info.java
new file mode 100644
index 000000000..2d660c200
--- /dev/null
+++ b/fine-byte-buddy/src/com/fr/third/org/newsclub/net/unix/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * The actual AF_UNIX Socket implementation.
+ */
+package com.fr.third.org.newsclub.net.unix;
+