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 classPath; + + /** + * Creates an external attachment. + * + * @param virtualMachineType The fully-qualified binary name of the virtual machine type. + * @param classPath The class path elements required for loading the supplied virtual machine type. + */ + public ExternalAttachment(String virtualMachineType, List classPath) { + this.virtualMachineType = virtualMachineType; + this.classPath = classPath; + } + + /** + * Returns the fully-qualified binary name of the virtual machine type. + * + * @return The fully-qualified binary name of the virtual machine type. + */ + public String getVirtualMachineType() { + return virtualMachineType; + } + + /** + * Returns the class path elements required for loading the supplied virtual machine type. + * + * @return The class path elements required for loading the supplied virtual machine type. + */ + public List getClassPath() { + return classPath; + } + } + + /** + * A simple implementation of an accessible accessor. + */ + @EqualsAndHashCode + abstract class Simple implements Accessor { + + /** + * A {@code VirtualMachine} class. + */ + protected final Class virtualMachineType; + + /** + * Creates a new simple accessor. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + protected Simple(Class virtualMachineType) { + this.virtualMachineType = virtualMachineType; + } + + /** + *

+ * 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.emptyList()); + } catch (ClassNotFoundException ignored) { + return Unavailable.INSTANCE; + } + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Class getVirtualMachineType() { + return virtualMachineType; + } + + /** + * A simple implementation of an accessible accessor that allows for external attachment. + */ + @EqualsAndHashCode(callSuper = true) + protected static class WithExternalAttachment extends Simple { + + /** + * The class path required for loading the virtual machine type. + */ + private final List classPath; + + /** + * Creates a new simple accessor that allows for external attachment. + * + * @param virtualMachineType The {@code com.sun.tools.attach.VirtualMachine} class. + * @param classPath The class path required for loading the virtual machine type. + */ + public WithExternalAttachment(Class virtualMachineType, List classPath) { + super(virtualMachineType); + this.classPath = classPath; + } + + @Override + public ExternalAttachment getExternalAttachment() { + return new ExternalAttachment(virtualMachineType.getName(), classPath); + } + } + + /** + * A simple implementation of an accessible accessor that does not allow for external attachment. + */ + @EqualsAndHashCode(callSuper = true) + protected static class WithoutExternalAttachment extends Simple { + + /** + * Creates a new simple accessor that does not allow for external attachment. + * + * @param virtualMachineType A {@code VirtualMachine} class. + */ + public WithoutExternalAttachment(Class virtualMachineType) { + super(virtualMachineType); + } + + @Override + public ExternalAttachment getExternalAttachment() { + throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); + } + } + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader. + */ + enum ForJigsawVm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public Accessor attempt() { + return Accessor.Simple.of(ClassLoader.getSystemClassLoader()); + } + } + + /** + * An attachment provider that locates the attach API directly from the system class loader expecting + * an IBM J9 VM. + */ + enum ForJ9Vm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public Accessor attempt() { + return Accessor.Simple.ofJ9(); + } + } + + /** + * An attachment provider that is dependant on the existence of a tools.jar file on the local + * file system. + */ + enum ForToolsJarVm implements AttachmentProvider { + + /** + * An attachment provider that locates the tools.jar from a Java home directory. + */ + JVM_ROOT("../lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar from a Java installation directory. + * In practice, several virtual machines do not return the JRE's location for the + * java.home property against the property's specification. + */ + JDK_ROOT("lib/tools.jar"), + + /** + * An attachment provider that locates the tools.jar as it is set for several JVM + * installations on Apple Macintosh computers. + */ + MACINTOSH("../Classes/classes.jar"); + + /** + * The Java home system property. + */ + private static final String JAVA_HOME_PROPERTY = "java.home"; + + /** + * The path to the tools.jar file, starting from the Java home directory. + */ + private final String toolsJarPath; + + /** + * Creates a new attachment provider that loads the virtual machine class from the tools.jar. + * + * @param toolsJarPath The path to the tools.jar file, starting from the Java home directory. + */ + ForToolsJarVm(String toolsJarPath) { + this.toolsJarPath = toolsJarPath; + } + + @Override + public Accessor attempt() { + File toolsJar = new File(System.getProperty(JAVA_HOME_PROPERTY), toolsJarPath); + try { + return toolsJar.isFile() && toolsJar.canRead() + ? Accessor.Simple.of(new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER), toolsJar) + : Accessor.Unavailable.INSTANCE; + } catch (MalformedURLException exception) { + throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); + } + } + } + + /** + * An attachment provider using a custom protocol implementation for HotSpot on Unix. + */ + enum ForUnixHotSpotVm implements AttachmentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public Accessor attempt() { + try { + return new Accessor.Simple.WithoutExternalAttachment(VirtualMachine.ForHotSpot.OnUnix.assertAvailability()); + } catch (Throwable ignored) { + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A compound attachment provider that attempts the attachment by delegation to other providers. If + * none of the providers of this compound provider is capable of providing a valid accessor, an + * non-available accessor is returned. + */ + @EqualsAndHashCode + class Compound implements AttachmentProvider { + + /** + * A list of attachment providers in the order of their application. + */ + private final List attachmentProviders; + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProvider A list of attachment providers in the order of their application. + */ + public Compound(AttachmentProvider... attachmentProvider) { + this(Arrays.asList(attachmentProvider)); + } + + /** + * Creates a new compound attachment provider. + * + * @param attachmentProviders A list of attachment providers in the order of their application. + */ + public Compound(List attachmentProviders) { + this.attachmentProviders = new ArrayList(); + for (AttachmentProvider attachmentProvider : attachmentProviders) { + if (attachmentProvider instanceof Compound) { + this.attachmentProviders.addAll(((Compound) attachmentProvider).attachmentProviders); + } else { + this.attachmentProviders.add(attachmentProvider); + } + } + } + + @Override + public Accessor attempt() { + for (AttachmentProvider attachmentProvider : attachmentProviders) { + Accessor accessor = attachmentProvider.attempt(); + if (accessor.isAvailable()) { + return accessor; + } + } + return Accessor.Unavailable.INSTANCE; + } + } + } + + /** + * A process provider is responsible for providing the process id of the current VM. + */ + public interface ProcessProvider { + + /** + * Resolves a process id for the current JVM. + * + * @return The resolved process id. + */ + String resolve(); + + /** + * Supplies the current VM's process id. + */ + enum ForCurrentVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The best process provider for the current VM. + */ + private final ProcessProvider dispatcher; + + /** + * Creates a process provider that supplies the current VM's process id. + */ + ForCurrentVm() { + dispatcher = ForJava9CapableVm.make(); + } + + @Override + public String resolve() { + return dispatcher.resolve(); + } + + /** + * A process provider for a legacy VM that reads the process id from its JMX properties. + */ + protected enum ForLegacyVm implements ProcessProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public String resolve() { + String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); + int processIdIndex = runtimeName.indexOf('@'); + if (processIdIndex == -1) { + throw new IllegalStateException("Cannot extract process id from runtime management bean"); + } else { + return runtimeName.substring(0, processIdIndex); + } + } + } + + /** + * A process provider for a Java 9 capable VM with access to the introduced process API. + */ + @EqualsAndHashCode + protected static class ForJava9CapableVm implements ProcessProvider { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new Java 9 capable dispatcher for reading the current process's id. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + /** + * Attempts to create a dispatcher for a Java 9 VM and falls back to a legacy dispatcher + * if this is not possible. + * + * @return A dispatcher for the current VM. + */ + public static ProcessProvider make() { + try { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } catch (Exception ignored) { + return ForLegacyVm.INSTANCE; + } + } + + @Override + public String resolve() { + try { + return pid.invoke(current.invoke(STATIC_MEMBER)).toString(); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getCause()); + } + } + } + } + } + + /** + * An agent provider is responsible for handling and providing the jar file of an agent that is being attached. + */ + protected interface AgentProvider { + + /** + * Provides an agent jar file for attachment. + * + * @return The provided agent. + * @throws IOException If the agent cannot be written to disk. + */ + File resolve() throws IOException; + + /** + * An agent provider for a temporary Byte Buddy agent. + */ + enum ForByteBuddyAgent implements AgentProvider { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The default prefix of the Byte Buddy agent jar file. + */ + private static final String AGENT_FILE_NAME = "byteBuddyAgent"; + + /** + * The jar file extension. + */ + private static final String JAR_FILE_EXTENSION = ".jar"; + + @Override + public File resolve() throws IOException { + File agentJar; + InputStream inputStream = Installer.class.getResourceAsStream('/' + Installer.class.getName().replace('.', '/') + CLASS_FILE_EXTENSION); + if (inputStream == null) { + throw new IllegalStateException("Cannot locate class file for Byte Buddy installer"); + } + try { + agentJar = File.createTempFile(AGENT_FILE_NAME, JAR_FILE_EXTENSION); + agentJar.deleteOnExit(); // Agent jar is required until VM shutdown due to lazy class loading. + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, MANIFEST_VERSION_VALUE); + manifest.getMainAttributes().put(new Attributes.Name(AGENT_CLASS_PROPERTY), Installer.class.getName()); + manifest.getMainAttributes().put(new Attributes.Name(CAN_REDEFINE_CLASSES_PROPERTY), Boolean.TRUE.toString()); + manifest.getMainAttributes().put(new Attributes.Name(CAN_RETRANSFORM_CLASSES_PROPERTY), Boolean.TRUE.toString()); + manifest.getMainAttributes().put(new Attributes.Name(CAN_SET_NATIVE_METHOD_PREFIX), Boolean.TRUE.toString()); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(agentJar), manifest); + try { + jarOutputStream.putNextEntry(new JarEntry(Installer.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(); + } + return agentJar; + } + } + + /** + * An agent provider that supplies an existing agent that is not deleted after attachment. + */ + @EqualsAndHashCode + class ForExistingAgent implements AgentProvider { + + /** + * The supplied agent. + */ + private File agent; + + /** + * Creates an agent provider for an existing agent. + * + * @param agent The supplied agent. + */ + protected ForExistingAgent(File agent) { + this.agent = agent; + } + + @Override + public File resolve() { + return agent; + } + } + } + + /** + * An attachment evaluator is responsible for deciding if an agent can be attached from the current process. + */ + protected interface AttachmentTypeEvaluator { + + /** + * Checks if the current VM requires external attachment for the supplied process id. + * + * @param processId The process id of the process to which to attach. + * @return {@code true} if the current VM requires external attachment for the supplied process. + */ + boolean requiresExternalAttachment(String processId); + + /** + * An installation action for creating an attachment type evaluator. + */ + enum InstallationAction implements PrivilegedAction { + + /** + * The singleton instance. + */ + INSTANCE; + + /** + * The OpenJDK's property for specifying the legality of self-attachment. + */ + private static final String JDK_ALLOW_SELF_ATTACH = "jdk.attach.allowAttachSelf"; + + @Override + public AttachmentTypeEvaluator run() { + try { + if (Boolean.getBoolean(JDK_ALLOW_SELF_ATTACH)) { + return Disabled.INSTANCE; + } else { + return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getMethod("current"), + Class.forName("java.lang.ProcessHandle").getMethod("pid")); + } + } catch (Exception ignored) { + return Disabled.INSTANCE; + } + } + } + + /** + * An attachment type evaluator that never requires external attachment. + */ + enum Disabled implements AttachmentTypeEvaluator { + + /** + * The singleton instance. + */ + INSTANCE; + + @Override + public boolean requiresExternalAttachment(String processId) { + return false; + } + } + + /** + * An attachment type evaluator that checks a process id against the current process id. + */ + @EqualsAndHashCode + class ForJava9CapableVm implements AttachmentTypeEvaluator { + + /** + * The {@code java.lang.ProcessHandle#current()} method. + */ + private final Method current; + + /** + * The {@code java.lang.ProcessHandle#pid()} method. + */ + private final Method pid; + + /** + * Creates a new attachment type evaluator. + * + * @param current The {@code java.lang.ProcessHandle#current()} method. + * @param pid The {@code java.lang.ProcessHandle#pid()} method. + */ + protected ForJava9CapableVm(Method current, Method pid) { + this.current = current; + this.pid = pid; + } + + @Override + public boolean requiresExternalAttachment(String processId) { + try { + return pid.invoke(current.invoke(STATIC_MEMBER)).toString().equals(processId); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access Java 9 process API", exception); + } catch (InvocationTargetException exception) { + throw new IllegalStateException("Error when accessing Java 9 process API", exception.getCause()); + } + } + } + } +} diff --git a/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Installer.java b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Installer.java new file mode 100644 index 000000000..93b907797 --- /dev/null +++ b/fine-byte-buddy/src/com/fr/third/net/bytebuddy/agent/Installer.java @@ -0,0 +1,71 @@ +package com.fr.third.net.bytebuddy.agent; + +import java.lang.instrument.Instrumentation; + +/** + * An installer class which defined the hook-in methods that are required by the Java agent specification. + */ +public class Installer { + + /** + * A field for carrying the {@link java.lang.instrument.Instrumentation} that was loaded by the Byte Buddy + * agent. Note that this field must never be accessed directly as the agent is injected into the VM's + * system class loader. This way, the field of this class might be {@code null} even after the installation + * of the Byte Buddy agent as this class might be loaded by a different class loader than the system class + * loader. + */ + @SuppressWarnings("unused") + private static volatile Instrumentation instrumentation; + + /** + * The installer provides only {@code static} hook-in methods and should not be instantiated. + */ + private Installer() { + throw new UnsupportedOperationException(); + } + + /** + *

+ * 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]"; + } + + /** + * Returns true 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; +