com.technophobia.substeps.runner.ForkedRunner.java Source code

Java tutorial

Introduction

Here is the source code for com.technophobia.substeps.runner.ForkedRunner.java

Source

/*
 *   Copyright Technophobia Ltd 2012
 *
 *   This file is part of Substeps.
 *
 *    Substeps is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    Substeps is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public License
 *    along with Substeps.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.technophobia.substeps.runner;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;

import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.technophobia.substeps.execution.node.RootNode;

public class ForkedRunner implements MojoRunner {

    private static final int START_TIMEOUT_SECONDS = 30;

    private final Log log;

    private Process forkedJVMProcess = null;

    private SubstepsJMXClient substepsJmxClient = new SubstepsJMXClient();

    private final int jmxPort;

    private final String vmArgs;

    private final List<String> testClasspathElements;

    private final List<String> stepImplementationArtifacts;

    private final ArtifactResolver artifactResolver;

    private final ArtifactFactory artifactFactory;

    private final MavenProjectBuilder mavenProjectBuilder;

    private final ArtifactRepository localRepository;

    private final List<ArtifactRepository> remoteRepositories;

    private final ArtifactMetadataSource artifactMetadataSource;

    private ForkedProcessCloser shutdownHook;

    private final InputStreamConsumer consumer;

    ForkedRunner(final Log log, final int jmxPort, final String vmArgs, final List<String> testClasspathElements,
            final List<String> stepImplementationArtifacts, final ArtifactResolver artifactResolver,
            final ArtifactFactory artifactFactory, final MavenProjectBuilder mavenProjectBuilder,
            final ArtifactRepository localRepository, final List<ArtifactRepository> remoteRepositories,
            final ArtifactMetadataSource artifactMetadataSource) throws MojoExecutionException {

        this.log = log;
        this.jmxPort = jmxPort;
        this.vmArgs = vmArgs;
        this.testClasspathElements = testClasspathElements;
        this.stepImplementationArtifacts = stepImplementationArtifacts;
        this.artifactResolver = artifactResolver;
        this.artifactFactory = artifactFactory;
        this.mavenProjectBuilder = mavenProjectBuilder;
        this.localRepository = localRepository;
        this.remoteRepositories = remoteRepositories;
        this.artifactMetadataSource = artifactMetadataSource;

        this.substepsJmxClient = new SubstepsJMXClient();

        this.consumer = startMBeanJVM();

        initialiseClient();
    }

    private void initialiseClient() throws MojoExecutionException {

        this.substepsJmxClient.init(this.jmxPort);
    }

    public void shutdown() {

        this.substepsJmxClient.shutdown();

        if (this.forkedJVMProcess != null) {

            try {
                this.log.info("waiting for forked process to return");

                final int waitFor = this.forkedJVMProcess.waitFor();

                this.log.info("wait for forked VM returned with exit code: " + waitFor);

                this.shutdownHook.notifyShutdownSuccessful();

                // now we can close the streams
                if (this.consumer != null) {
                    this.consumer.closeStreams();
                }

            } catch (final InterruptedException e) {
                // not sure what we can do at this point...
                e.printStackTrace();
            }
        }

        this.log.info("forked process returned");

    }

    private InputStreamConsumer startMBeanJVM() throws MojoExecutionException {
        // launch the jvm process that will contain the Substeps MBean Server
        // build up the class path based on this projects classpath

        final CountDownLatch processStarted = new CountDownLatch(1);
        final AtomicBoolean processStartedOk = new AtomicBoolean(false);

        InputStreamConsumer consumer = null;

        final List<String> command = buildSubstepsRunnerCommand();

        final ProcessBuilder processBuilder = new ProcessBuilder(command);

        processBuilder.redirectErrorStream(true);

        try {

            this.log.debug(
                    "Starting substeps process with command " + Joiner.on(" ").join(processBuilder.command()));

            this.forkedJVMProcess = processBuilder.start();

            // need to add the shutdown hook straight away
            this.shutdownHook = ForkedProcessCloser.addHook(this.substepsJmxClient, this.forkedJVMProcess,
                    this.log);

            consumer = new InputStreamConsumer(this.forkedJVMProcess.getInputStream(), this.log, processStarted,
                    processStartedOk);

            final Thread t = new Thread(consumer);
            t.start();

        } catch (final IOException e) {

            e.printStackTrace();
        }

        boolean exceptionThrown = false;
        try {
            this.log.info("waiting for process to start...");
            processStarted.await(START_TIMEOUT_SECONDS, TimeUnit.SECONDS);

            if (!processStartedOk.get()) {
                exceptionThrown = true;
                throw new MojoExecutionException("Unable to launch VM process");
            }

            this.log.info("process started");
        } catch (final InterruptedException e) {

            e.printStackTrace();
        }

        return consumer;
    }

    /**
     * @param cpBuf
     * @return
     * @throws MojoExecutionException
     * @throws DependencyResolutionRequiredException
     */
    private List<String> buildSubstepsRunnerCommand() throws MojoExecutionException {

        final String classpath = createClasspathString();

        final List<String> command = Lists.newArrayList();

        // attempt to use JAVA_HOME
        String javaHome = System.getenv("JAVA_HOME");
        if (javaHome == null) {
            javaHome = System.getenv("java_home");
        }

        if (javaHome == null) {
            // not sure how we'd get here - maven running without JAVA_HOME
            // set..??
            this.log.warn("unable to resolve JAVA_HOME variable, assuming java is on the path...");
            command.add("java");
        } else {
            command.add(javaHome + File.separator + "bin" + File.separator + "java");
        }

        command.add("-Dfile.encoding=UTF-8");
        command.add("-Dcom.sun.management.jmxremote.port=" + this.jmxPort);
        command.add("-Dcom.sun.management.jmxremote.authenticate=false");
        command.add("-Dcom.sun.management.jmxremote.ssl=false");
        command.add("-Djava.rmi.server.hostname=localhost");

        addCurrentVmArgs(command);

        if (this.vmArgs != null && !this.vmArgs.isEmpty()) {
            final String[] args = this.vmArgs.split(" ");
            for (final String arg : args) {
                command.add(arg);
                this.log.info("Adding jvm arg: " + arg);
            }
        }

        command.add("-classpath");
        command.add(classpath);
        command.add("com.technophobia.substeps.jmx.SubstepsJMXServer");
        return command;
    }

    @SuppressWarnings("unchecked")
    private void addCurrentVmArgs(final List<String> command) {

        for (final String key : (List<String>) Collections.list(System.getProperties().propertyNames())) {

            command.add("-D" + key + "=" + System.getProperty(key));
        }

    }

    private String createClasspathString() throws MojoExecutionException {

        final List<String> classPathElements = Lists.newArrayList();

        classPathElements.addAll(this.testClasspathElements);
        classPathElements.addAll(resolveStepImplementationArtifacts());

        return Joiner.on(File.pathSeparator).join(classPathElements);
    }

    @SuppressWarnings("unchecked")
    private List<String> resolveStepImplementationArtifacts() throws MojoExecutionException {

        final List<String> stepImplementationArtifactJars = Lists.newArrayList();
        if (this.stepImplementationArtifacts != null) {
            for (final String stepImplementationArtifactString : this.stepImplementationArtifacts) {

                final String[] artifactDetails = stepImplementationArtifactString.split(":");

                if (artifactDetails.length != 3) {
                    throw new MojoExecutionException(
                            "Invalid artifact format found in substepImplementationArtifact, must be in format groupId:artifactId:version but was '"
                                    + stepImplementationArtifactString + "'");
                }

                try {

                    final Artifact stepImplementationJarArtifact = this.artifactFactory.createArtifact(
                            artifactDetails[0], artifactDetails[1], artifactDetails[2], "test", "jar");
                    this.artifactResolver.resolve(stepImplementationJarArtifact, this.remoteRepositories,
                            this.localRepository);

                    addArtifactPath(stepImplementationArtifactJars, stepImplementationJarArtifact);

                    final Artifact stepImplementationPomArtifact = this.artifactFactory.createArtifact(
                            artifactDetails[0], artifactDetails[1], artifactDetails[2], "test", "pom");
                    this.artifactResolver.resolve(stepImplementationPomArtifact, this.remoteRepositories,
                            this.localRepository);
                    final MavenProject stepImplementationProject = this.mavenProjectBuilder.buildFromRepository(
                            stepImplementationPomArtifact, this.remoteRepositories, this.localRepository);
                    final Set<Artifact> stepImplementationArtifacts = stepImplementationProject
                            .createArtifacts(this.artifactFactory, null, null);

                    final Set<Artifact> transitiveDependencies = this.artifactResolver
                            .resolveTransitively(stepImplementationArtifacts, stepImplementationPomArtifact,
                                    stepImplementationProject.getManagedVersionMap(), this.localRepository,
                                    this.remoteRepositories, this.artifactMetadataSource)
                            .getArtifacts();

                    for (final Artifact transitiveDependency : transitiveDependencies) {
                        addArtifactPath(stepImplementationArtifactJars, transitiveDependency);
                    }

                } catch (final ArtifactResolutionException e) {

                    throw new MojoExecutionException("Unable to resolve artifact for substep implementation '"
                            + stepImplementationArtifactString + "'", e);

                } catch (final ProjectBuildingException e) {

                    throw new MojoExecutionException("Unable to resolve artifact for substep implementation '"
                            + stepImplementationArtifactString + "'", e);
                } catch (final InvalidDependencyVersionException e) {

                    throw new MojoExecutionException("Unable to resolve artifact for substep implementation '"
                            + stepImplementationArtifactString + "'", e);
                } catch (final ArtifactNotFoundException e) {

                    throw new MojoExecutionException("Unable to resolve artifact for substep implementation '"
                            + stepImplementationArtifactString + "'", e);
                }

            }
        }
        return stepImplementationArtifactJars;
    }

    private void addArtifactPath(final List<String> stepImplementationArtifactJars, final Artifact artifact) {
        final String path = artifact.getFile().getPath();
        this.log.info("Adding dependency to classpath for forked jvm: " + path);
        stepImplementationArtifactJars.add(path);
    }

    public RootNode prepareExecutionConfig(final SubstepsExecutionConfig theConfig) {

        return this.substepsJmxClient.prepareExecutionConfig(theConfig);
    }

    public RootNode run() {

        this.log.info("Running substeps tests in forked jvm");
        return this.substepsJmxClient.run();
    }

    public List<SubstepExecutionFailure> getFailures() {

        return this.substepsJmxClient.getFailures();
    }

    public void addNotifier(final INotifier notifier) {

        this.substepsJmxClient.addNotifier(notifier);
    }

}