All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glowroot.instrumentation.test.harness.impl.JavaagentContainer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.glowroot.instrumentation.test.harness.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Joiner;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Splitter;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.StandardSystemProperty;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Stopwatch;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Strings;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.collect.ImmutableList;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.collect.Lists;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.io.ByteStreams;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.glowroot.instrumentation.test.harness.shaded.org.slf4j.Logger;
import org.glowroot.instrumentation.test.harness.shaded.org.slf4j.LoggerFactory;

import org.glowroot.instrumentation.test.harness.AppUnderTest;
import org.glowroot.instrumentation.test.harness.Container;
import org.glowroot.instrumentation.test.harness.IncomingSpan;
import org.glowroot.instrumentation.test.harness.agent.Premain;
import org.glowroot.instrumentation.test.harness.util.ConsoleOutputPipe;
import org.glowroot.instrumentation.test.harness.util.TempDirs;

import static org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

public class JavaagentContainer implements Container {

    private static final boolean XDEBUG = Boolean.getBoolean("test.harness.xdebug");

    private static final Logger logger = LoggerFactory.getLogger(JavaagentContainer.class);

    private final HeartbeatListener heartbeatListener;
    private final ExecutorService heartbeatListenerExecutor;
    private final File tmpDir;
    private final TraceCollector traceCollector;
    private final JavaagentClient javaagentClient;
    private final ExecutorService consolePipeExecutor;
    private final Future consolePipeFuture;
    private final Process process;
    private final ConsoleOutputPipe consoleOutputPipe;
    private final Thread shutdownHook;

    public static JavaagentContainer create() throws Exception {
        return new JavaagentContainer(ImmutableList.of());
    }

    public static JavaagentContainer createWithExtraJvmArgs(List extraJvmArgs)
            throws Exception {
        return new JavaagentContainer(extraJvmArgs);
    }

    private JavaagentContainer(List extraJvmArgs) throws Exception {

        // need to start heartbeat socket listener before spawning process
        heartbeatListener = new HeartbeatListener();
        heartbeatListenerExecutor = Executors.newSingleThreadExecutor();
        heartbeatListenerExecutor.execute(heartbeatListener);

        traceCollector = new TraceCollector();
        traceCollector.start();
        tmpDir = TempDirs.createTempDir("harness-dir");
        List command = buildCommand(heartbeatListener.serverSocket.getLocalPort(),
                traceCollector.getPort(), tmpDir, extraJvmArgs);
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        consolePipeExecutor = Executors.newSingleThreadExecutor();
        InputStream in = process.getInputStream();
        // process.getInputStream() only returns null if ProcessBuilder.redirectOutput() is used
        // to redirect output to a file
        checkNotNull(in);
        consoleOutputPipe = new ConsoleOutputPipe(in, System.out);
        consolePipeFuture = consolePipeExecutor.submit(consoleOutputPipe);
        this.process = process;

        Stopwatch stopwatch = Stopwatch.createStarted();
        javaagentClient = connectToJavaagent(heartbeatListener.getJavaagentServerPort(), stopwatch);
        javaagentClient.resetInstrumentationProperties();
        shutdownHook = new ShutdownHookThread(javaagentClient);
        // unfortunately, ctrl-c during maven test will kill the maven process, but won't kill the
        // forked surefire jvm where the tests are being run
        // (http://jira.codehaus.org/browse/SUREFIRE-413), and so this hook won't get triggered by
        // ctrl-c while running tests under maven
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    private static JavaagentClient connectToJavaagent(int javaagentServerPort, Stopwatch stopwatch)
            throws Exception {
        // this can take a while on slow travis ci build machines
        Exception lastException = null;
        while (stopwatch.elapsed(SECONDS) < 30) {
            try {
                return new JavaagentClient(javaagentServerPort);
            } catch (Exception e) {
                logger.debug(e.getMessage(), e);
                lastException = e;
            }
            MILLISECONDS.sleep(10);
        }
        throw checkNotNull(lastException);
    }

    @Override
    public void setInstrumentationProperty(String instrumentationId, String propertyName,
            boolean propertyValue) throws Exception {
        javaagentClient.setInstrumentationProperty(instrumentationId, propertyName, propertyValue);
    }

    @Override
    public void setInstrumentationProperty(String instrumentationId, String propertyName,
            Double propertyValue) throws Exception {
        javaagentClient.setInstrumentationProperty(instrumentationId, propertyName, propertyValue);
    }

    @Override
    public void setInstrumentationProperty(String instrumentationId, String propertyName,
            String propertyValue) throws Exception {
        javaagentClient.setInstrumentationProperty(instrumentationId, propertyName, propertyValue);
    }

    @Override
    public void setInstrumentationProperty(String instrumentationId, String propertyName,
            List propertyValue) throws Exception {
        // converting to ArrayList since guava ImmutableList may be shaded in javaagent and so would
        // error on de-serialization
        javaagentClient.setInstrumentationProperty(instrumentationId, propertyName,
                new ArrayList(propertyValue));
    }

    @Override
    public IncomingSpan execute(Class appClass, Serializable... args)
            throws Exception {
        return executeInternal(appClass, null, null, args);
    }

    @Override
    public IncomingSpan executeForType(Class appClass,
            String transactionType, Serializable... args) throws Exception {
        return executeInternal(appClass, transactionType, null, args);
    }

    @Override
    public IncomingSpan executeForTypeAndName(Class appClass,
            String transactionType, String transactionName, Serializable... args) throws Exception {
        return executeInternal(appClass, transactionType, transactionName, args);
    }

    @Override
    public void executeNoExpectedTrace(Class appClass, Serializable... args)
            throws Exception {
        executeInternal(appClass, args);
        // give a short time to see if trace gets collected
        MILLISECONDS.sleep(10);
        if (traceCollector != null && traceCollector.hasIncomingSpan()) {
            throw new IllegalStateException("Trace was collected when none was expected");
        }
    }

    @Override
    public void resetAfterEachTest() throws Exception {
        javaagentClient.resetInstrumentationProperties();
    }

    @Override
    public void close() throws Exception {
        heartbeatListener.closed = true;
        javaagentClient.kill();
        traceCollector.close();
        process.waitFor();
        consolePipeFuture.get();
        consolePipeExecutor.shutdown();
        if (!consolePipeExecutor.awaitTermination(10, SECONDS)) {
            throw new IllegalStateException("Could not terminate executor");
        }
        heartbeatListenerExecutor.shutdown();
        if (!heartbeatListenerExecutor.awaitTermination(10, SECONDS)) {
            throw new IllegalStateException("Could not terminate executor");
        }
        heartbeatListener.serverSocket.close();
        Runtime.getRuntime().removeShutdownHook(shutdownHook);
        TempDirs.deleteRecursively(tmpDir);
    }

    private IncomingSpan executeInternal(Class appClass,
            @Nullable String transactionType, @Nullable String transactionName,
            Serializable... args) throws Exception {
        checkNotNull(traceCollector);
        executeInternal(appClass, args);
        // extra long wait time is needed for StackOverflowOOMIT on slow travis ci machines since it
        // can sometimes take a long time for that large trace to be serialized and transferred
        IncomingSpan incomingSpan =
                traceCollector.getCompletedIncomingSpan(transactionType, transactionName, 20,
                        SECONDS);
        traceCollector.clearIncomingSpans();
        return incomingSpan;
    }

    private void executeInternal(Class appUnderTestClass,
            Serializable[] args) throws Exception {
        javaagentClient.executeApp(appUnderTestClass.getName(), args);
    }

    private static List buildCommand(int heartbeatPort, int collectorPort, File tmpDir,
            List extraJvmArgs) throws Exception {
        List command = Lists.newArrayList();
        String javaExecutable = StandardSystemProperty.JAVA_HOME.value() + File.separator + "bin"
                + File.separator + "java";
        command.add(javaExecutable);
        boolean hasXmx = false;
        for (String extraJvmArg : extraJvmArgs) {
            command.add(extraJvmArg);
            if (extraJvmArg.startsWith("-Xmx")) {
                hasXmx = true;
            }
        }
        // it is important for jacoco javaagent to be prior to the test harness javaagent so that
        // jacoco will use original class bytes to form its class id at runtime which will then
        // match up with the class id at analysis time
        command.addAll(getJacocoArgsFromCurrentJvm());
        String classpath = Strings.nullToEmpty(StandardSystemProperty.JAVA_CLASS_PATH.value());
        List bootPaths = Lists.newArrayList();
        List paths = Lists.newArrayList();
        List maybeBootPaths = Lists.newArrayList();
        File delegatingJavaagentJarFile = null;
        File javaagentJarFile = null;
        for (String path : Splitter.on(File.pathSeparatorChar).split(classpath)) {
            File file = new File(path);
            String name = file.getName();
            String targetClasses = File.separator + "target" + File.separator + "classes";
            if (name.matches("delegating-javaagent-[0-9.]+(-SNAPSHOT)?.jar")) {
                delegatingJavaagentJarFile = file;
            } else if (name.matches("instrumentation-test-harness-[0-9.]+(-SNAPSHOT)?.jar")) {
                javaagentJarFile = file;
            } else if (name.matches("instrumentation-api-[0-9.]+(-SNAPSHOT)?.jar")
                    || name.matches("instrumentation-engine-[0-9.]+(-SNAPSHOT)?.jar")) {
                // these are instrumentation-test-harness transitive dependencies
                maybeBootPaths.add(path);
            } else if (file.getAbsolutePath()
                    .endsWith(File.separator + "instrumentation-api" + targetClasses)
                    || file.getAbsolutePath().endsWith(File.separator + "engine" + targetClasses)) {
                // these are instrumentation-test-harness transitive dependencies
                maybeBootPaths.add(path);
            } else if (name.matches("asm-.*\\.jar")
                    || name.matches("guava-.*\\.jar")
                    || name.matches("gson-.*\\.jar")
                    || name.matches("logback-.*\\.jar")
                    // javax.servlet-api is needed because logback-classic has
                    // META-INF/services/javax.servlet.ServletContainerInitializer
                    || name.matches("javax.servlet-api-.*\\.jar")
                    || name.matches("slf4j-api-.*\\.jar")
                    || name.matches("value-.*\\.jar")
                    // this is needed for now to support reusable ExecuteHttpBase
                    || name.matches("nanohttpd-.*\\.jar")
                    || name.matches("error_prone_annotations-.*\\.jar")
                    || name.matches("jsr305-.*\\.jar")) {
                // these are instrumentation-test-harness transitive dependencies
                maybeBootPaths.add(path);
            } else if (name.endsWith(".jar") && file.getAbsolutePath()
                    .endsWith(File.separator + "target" + File.separator + name)) {
                // this is the instrumentation under test
                bootPaths.add(path);
            } else if (name.matches("instrumentation-[a-z0-9-]+-[0-9.]+(-SNAPSHOT)?.jar")) {
                // this another (core) instrumentation that it depends on, e.g. the executor
                // instrumentation
                bootPaths.add(path);
            } else if (file.getAbsolutePath().endsWith(targetClasses)) {
                // this is the instrumentation under test
                bootPaths.add(path);
            } else if (file.getAbsolutePath()
                    .endsWith(File.separator + "target" + File.separator + "test-classes")) {
                // this is the instrumentation test classes
                paths.add(path);
            } else {
                // these are instrumentation test dependencies
                paths.add(path);
            }
        }
        if (javaagentJarFile == null) {
            bootPaths.addAll(maybeBootPaths);
        } else {
            boolean shaded = false;
            JarInputStream jarIn = new JarInputStream(new FileInputStream(javaagentJarFile));
            try {
                JarEntry jarEntry;
                while ((jarEntry = jarIn.getNextJarEntry()) != null) {
                    if (jarEntry.getName()
                            .startsWith("org/glowroot/instrumentation/test/harness/shaded/")) {
                        shaded = true;
                        break;
                    }
                }
            } finally {
                jarIn.close();
            }
            if (shaded) {
                paths.addAll(maybeBootPaths);
            } else {
                bootPaths.addAll(maybeBootPaths);
            }
        }
        command.add("-Xbootclasspath/a:" + Joiner.on(File.pathSeparatorChar).join(bootPaths));
        command.add("-classpath");
        command.add(Joiner.on(File.pathSeparatorChar).join(paths));
        if (XDEBUG) {
            // the -agentlib arg needs to come before the -javaagent arg
            command.add("-Xdebug");
            command.add("-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=y");
        }
        if (javaagentJarFile == null) {
            javaagentJarFile = checkNotNull(delegatingJavaagentJarFile);
        }
        command.add("-javaagent:" + javaagentJarFile + "=" + Premain.class.getName());
        command.add("-Dtest.harness.tmpDir=" + tmpDir.getAbsolutePath());
        command.add("-Dtest.harness.collectorPort=" + collectorPort);
        // this is used inside low-entropy docker containers
        String sourceOfRandomness = System.getProperty("java.security.egd");
        if (sourceOfRandomness != null) {
            command.add("-Djava.security.egd=" + sourceOfRandomness);
        }
        if (!hasXmx) {
            command.add("-Xmx" + Runtime.getRuntime().maxMemory());
        }
        // leave as much memory as possible to old gen
        command.add("-XX:NewRatio=20");
        command.add(JavaagentMain.class.getName());
        command.add(Integer.toString(heartbeatPort));
        return command;
    }

    private static List getJacocoArgsFromCurrentJvm() {
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        List arguments = runtimeMXBean.getInputArguments();
        List jacocoArgs = Lists.newArrayList();
        for (String argument : arguments) {
            if (argument.startsWith("-javaagent:") && argument.contains("jacoco")) {
                jacocoArgs.add(argument + ",includes=org.glowroot.instrumentation.*");
                break;
            }
        }
        return jacocoArgs;
    }

    private static class HeartbeatListener implements Runnable {

        private final ServerSocket serverSocket;

        private final AtomicInteger javaagentServerPort = new AtomicInteger();

        private volatile boolean closed;

        private HeartbeatListener() throws IOException {
            serverSocket = new ServerSocket(0);
        }

        @Override
        public void run() {
            try {
                Socket socket = serverSocket.accept();
                ObjectInputStream socketIn = new ObjectInputStream(socket.getInputStream());
                synchronized (javaagentServerPort) {
                    javaagentServerPort.set(socketIn.readInt());
                    javaagentServerPort.notifyAll();
                }
                ByteStreams.exhaust(socketIn);
            } catch (IOException e) {
                if (!closed) {
                    logger.error(e.getMessage(), e);
                }
            }
        }

        private int getJavaagentServerPort() throws InterruptedException {
            synchronized (javaagentServerPort) {
                while (javaagentServerPort.get() == 0) {
                    javaagentServerPort.wait();
                }
            }
            return javaagentServerPort.get();
        }
    }

    private static class ShutdownHookThread extends Thread {

        private final JavaagentClient javaagentClient;

        private ShutdownHookThread(JavaagentClient javaagentClient) {
            this.javaagentClient = javaagentClient;
        }

        @Override
        public void run() {
            try {
                javaagentClient.kill();
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy