001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.aop.agent;
031
032import java.io.File;
033import java.io.FileOutputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.OutputStream;
037import java.io.UnsupportedEncodingException;
038import java.lang.instrument.Instrumentation;
039import java.lang.management.ManagementFactory;
040import java.lang.reflect.Method;
041import java.net.URL;
042import java.net.URLClassLoader;
043import java.security.AccessController;
044import java.security.PrivilegedActionException;
045import java.security.PrivilegedExceptionAction;
046import java.util.ArrayList;
047import java.util.List;
048import java.util.jar.JarEntry;
049import java.util.jar.JarOutputStream;
050
051import org.apache.log4j.Logger;
052
053/**
054 * Dynamic agent loader. Provides methods to extract an agent jar with
055 * a specific agent class, and to attempt to dynamically load agents
056 * on Oracle JVMs (including OpenJDK). Dynamic loading won't work on
057 * all JVMs (i.e. IBMs), but the standard "-javaagent" commandline option
058 * can be used instead. If dynamic loading fails, instructions on using 
059 * the command line flag will be printed to stderr.
060 * 
061 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
062 */
063public class AgentLoader {
064        private static final Logger logger = Logger.getLogger(AgentLoader.class);
065        private static final String VMCLASS = "com.sun.tools.attach.VirtualMachine"; 
066
067        private static long copy(InputStream input, OutputStream output) throws IOException {
068                long count = 0;
069                int n = 0;
070                byte[] buffer = new byte[4096];
071
072                while (-1 != (n = input.read(buffer))) {
073                        output.write(buffer, 0, n);
074                        count += n;
075                }
076
077                return count;
078        }
079
080        private static byte[] createManifest(Class<?> agentClass) {
081                StringBuffer sb = new StringBuffer();
082
083                try {
084                        agentClass.getDeclaredMethod("premain", String.class, Instrumentation.class);
085                        sb.append("Premain-Class: "+ agentClass.getName() + "\n");
086                } catch (NoSuchMethodException e) { 
087                        //IGNORE//
088                }
089
090                try {
091                        agentClass.getDeclaredMethod("agentmain", String.class, Instrumentation.class);
092                        sb.append("Agent-Class: "+ agentClass.getName() + "\n");
093                } catch (NoSuchMethodException e) { 
094                        //IGNORE//
095                }
096
097                sb.append("Can-Redefine-Classes: true\n");
098                sb.append("Can-Retransform-Classes: true\n");
099
100                try {
101                        return sb.toString().getBytes("US-ASCII");
102                } catch (UnsupportedEncodingException e) {
103                        throw new RuntimeException("Charset US-ASCII isn't supported!! This should never happen.");
104                }
105        }
106
107        /**
108         * Create an agent jar file with the required manifest entries.
109         * 
110         * @param file the location to create the jar
111         * @param agentClass the agent class
112         * @throws IOException if an error occurs
113         */
114        public static void createAgentJar(File file, Class<?> agentClass) throws IOException {
115                JarOutputStream jos = new JarOutputStream(new FileOutputStream(file));
116
117                String classEntryPath = agentClass.getName().replace(".", "/") + ".class";
118                InputStream classStream = agentClass.getClassLoader().getResourceAsStream(classEntryPath);
119
120                if (classEntryPath.startsWith("/")) classEntryPath = classEntryPath.substring(1);
121
122                JarEntry entry = new JarEntry(classEntryPath);
123                jos.putNextEntry(entry);
124                copy(classStream, jos);
125                jos.closeEntry();
126
127                entry = new JarEntry("META-INF/MANIFEST.MF");
128                jos.putNextEntry(entry);
129                jos.write(createManifest(agentClass));
130                jos.closeEntry();
131
132                jos.close();
133        }
134
135        /**
136         * Attempt to locate potential "tools.jar" jars
137         */
138        private static List<File> getPotentialToolsJars() {
139                List<File> jars = new ArrayList<File>();
140
141                File javaHome = new File(System.getProperty("java.home"));
142
143                File jreSourced = new File(javaHome, "lib/tools.jar");
144                if (jreSourced.exists()) {
145                        jars.add(jreSourced);
146                }
147
148                if ("jre".equals(javaHome.getName())) {
149                        File jdkHome = new File(javaHome, "../");
150                        File jdkSourced = new File(jdkHome, "lib/tools.jar");
151                        if (jdkSourced.exists()) {
152                                jars.add(jdkSourced);
153                        }
154                }
155
156                return jars;
157        }
158
159        /**
160         * Try and get the VirtualMachine class
161         */
162        private static Class<?> tryGetVMClass() {
163                try {
164                        return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
165                                @Override
166                                public Class<?> run() throws Exception {
167                                        try {
168                                                return ClassLoader.getSystemClassLoader().loadClass(VMCLASS);
169                                        } catch (ClassNotFoundException e) {
170                                                for (File jar : getPotentialToolsJars()) {
171                                                        try {
172                                                                return new URLClassLoader(new URL[] {jar.toURI().toURL()}).loadClass(VMCLASS);
173                                                        } catch (Throwable t) {
174                                                                logger.trace("Exception while loading tools.jar from "+ jar, t);
175                                                        }
176                                                }
177                                        }
178                                        return null;
179                                }
180                        });
181                } catch (PrivilegedActionException pae) {
182                        Throwable actual = pae.getCause();
183
184                        if (actual instanceof ClassNotFoundException) {
185                                logger.trace("No VirtualMachine found");
186                                return null;
187                        }
188
189                        throw new RuntimeException("Unexpected checked exception : " + actual);
190                }
191        }
192
193        private static void loadFailed() {
194                System.err.println("Unable to load the java agent dynamically");
195                //FIXME: instructions
196        }
197
198        /**
199         * Attempt to dynamically load the given agent class
200         * 
201         * @param agentClass the agent class
202         * @throws IOException if an error occurs creating the agent jar
203         */
204        public static void loadAgent(Class<?> agentClass) throws IOException {
205                File tmp = File.createTempFile("agent", ".jar");
206                tmp.deleteOnExit();
207                createAgentJar(tmp, agentClass);
208
209                String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
210                int p = nameOfRunningVM.indexOf('@');
211                String pid = nameOfRunningVM.substring(0, p);
212
213                Class<?> vmClass = tryGetVMClass();
214
215                if (vmClass == null) {
216                        loadFailed();
217                } else {
218                        try {
219                                Method attach = vmClass.getMethod("attach", String.class);
220                                Method loadAgent = vmClass.getMethod("loadAgent", String.class);
221                                Method detach = vmClass.getMethod("detach");
222                                
223                                Object vm = attach.invoke(null, pid);
224                                try {
225                                        loadAgent.invoke(vm, tmp.getAbsolutePath());
226                                } finally {
227                                        detach.invoke(vm);
228                                }
229                        } catch (Exception e) {
230                                logger.warn("Loading the agent failed", e);
231                                loadFailed();
232                        }
233                }
234        }
235}