Commit 24078886 authored by Dave Syer's avatar Dave Syer

Make RunMojo fork a new process

By forking a new process we get to attach the agent
much earlier and JPA can co-exist.

Fixes gh-648
parent f53ee406
...@@ -43,6 +43,13 @@ ...@@ -43,6 +43,13 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
</dependencies>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId> <artifactId>springloaded</artifactId>
<version>1.1.5.RELEASE</version> <version>1.2.0.RELEASE</version>
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
......
/*
* Copyright 2012-2014 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.springframework.boot.loader.tools;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Provides access to the java binary executable, regardless of OS.
*
* @author Phillip Webb
*/
public class JavaExecutable {
private File file;
public JavaExecutable() {
String javaHome = System.getProperty("java.home");
Assert.state(StringUtils.hasLength(javaHome),
"Unable to find java executable due to missing 'java.home'");
this.file = findInJavaHome(javaHome);
}
private File findInJavaHome(String javaHome) {
File bin = new File(new File(javaHome), "bin");
File command = new File(bin, "java.exe");
command = (command.exists() ? command : new File(bin, "java"));
Assert.state(command.exists(), "Unable to find java in " + javaHome);
return command;
}
// TODO: is this used?
public ProcessBuilder processBuilder(String... arguments) {
ProcessBuilder processBuilder = new ProcessBuilder(toString());
processBuilder.command().addAll(Arrays.asList(arguments));
return processBuilder;
}
@Override
public String toString() {
try {
return this.file.getCanonicalPath();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
/*
* Copyright 2012-2014 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.springframework.boot.loader.tools;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.util.ReflectionUtils;
/**
* Special utility used to run a process.
*
* @author Phillip Webb
* @author Dave Syer
*/
public class RunProcess {
private static final Method INHERIT_IO_METHOD = ReflectionUtils.findMethod(
ProcessBuilder.class, "inheritIO");
private static final long JUST_ENDED_LIMIT = 500;
private final String[] command;
private volatile Process process;
private volatile long endTime;
public RunProcess(String... command) {
this.command = command;
}
public void run(String... args) throws Exception {
run(Arrays.asList(args));
}
protected void run(Collection<String> args) throws IOException {
ProcessBuilder builder = new ProcessBuilder(this.command);
builder.command().addAll(args);
builder.redirectErrorStream(true);
boolean inheritedIO = inheritIO(builder);
try {
this.process = builder.start();
if (!inheritedIO) {
redirectOutput(this.process);
}
try {
this.process.waitFor();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
finally {
this.endTime = System.currentTimeMillis();
this.process = null;
}
}
private boolean inheritIO(ProcessBuilder builder) {
try {
INHERIT_IO_METHOD.invoke(builder);
return true;
}
catch (Exception ex) {
return false;
}
}
private void redirectOutput(Process process) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(
process.getInputStream()));
new Thread() {
@Override
public void run() {
try {
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();
}
catch (Exception ex) {
}
};
}.start();
}
/**
* @return the running process or {@code null}
*/
public Process getRunningProcess() {
return this.process;
}
/**
* @return {@code true} if the process was stopped.
*/
public boolean handleSigInt() {
// if the process has just ended, probably due to this SIGINT, consider handled.
if (hasJustEnded()) {
return true;
}
// destroy the running process
Process process = this.process;
if (process != null) {
try {
process.destroy();
process.waitFor();
this.process = null;
return true;
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
return false;
}
public boolean hasJustEnded() {
return System.currentTimeMillis() < (this.endTime + JUST_ENDED_LIMIT);
}
}
...@@ -18,10 +18,8 @@ package org.springframework.boot.maven; ...@@ -18,10 +18,8 @@ package org.springframework.boot.maven;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource; import java.security.CodeSource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -37,9 +35,10 @@ import org.apache.maven.plugins.annotations.Mojo; ...@@ -37,9 +35,10 @@ import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.springframework.boot.loader.tools.AgentAttacher;
import org.springframework.boot.loader.tools.FileUtils; import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JavaExecutable;
import org.springframework.boot.loader.tools.MainClassFinder; import org.springframework.boot.loader.tools.MainClassFinder;
import org.springframework.boot.loader.tools.RunProcess;
/** /**
* MOJO that can be used to run a executable archive application directly from Maven. * MOJO that can be used to run a executable archive application directly from Maven.
...@@ -106,17 +105,6 @@ public class RunMojo extends AbstractMojo { ...@@ -106,17 +105,6 @@ public class RunMojo extends AbstractMojo {
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException { public void execute() throws MojoExecutionException, MojoFailureException {
findAgent();
if (this.agent != null) {
getLog().info("Attaching agent: " + this.agent);
if (this.noverify != null && this.noverify && !AgentAttacher.hasNoVerify()) {
throw new MojoExecutionException(
"The JVM must be started with -noverify for "
+ "this agent to work. You can use MAVEN_OPTS=-noverify "
+ "to add that flag.");
}
AgentAttacher.attach(this.agent);
}
final String startClassName = getStartClass(); final String startClassName = getStartClass();
run(startClassName); run(startClassName);
} }
...@@ -139,16 +127,43 @@ public class RunMojo extends AbstractMojo { ...@@ -139,16 +127,43 @@ public class RunMojo extends AbstractMojo {
catch (ClassNotFoundException ex) { catch (ClassNotFoundException ex) {
// ignore; // ignore;
} }
if (this.noverify == null) {
this.noverify = false;
}
} }
private void run(String startClassName) throws MojoExecutionException { private void run(String startClassName) throws MojoExecutionException {
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName); findAgent();
Thread launchThread = new Thread(threadGroup, new LaunchRunner(startClassName, int extras = 0;
this.arguments), startClassName + ".main()"); if (this.agent != null) {
launchThread.setContextClassLoader(getClassLoader()); getLog().info("Attaching agent: " + this.agent);
launchThread.start(); extras = 1;
join(threadGroup); }
threadGroup.rethrowUncaughtException(); if (this.noverify) {
extras++;
}
String[] args = new String[this.arguments.length + extras + 3];
System.arraycopy(this.arguments, 0, args, extras + 3, this.arguments.length);
if (extras > 0) {
args[0] = "-javaagent:" + this.agent;
}
if (this.noverify) {
args[1] = "-noverify";
}
args[extras + 2] = startClassName;
try {
StringBuilder classpath = new StringBuilder();
for (URL ele : getClassPathUrls()) {
classpath = classpath.append((classpath.length() > 0 ? File.pathSeparator
: "") + ele);
}
args[extras] = "-cp";
args[extras + 1] = classpath.toString();
new RunProcess(new JavaExecutable().toString()).run(args);
}
catch (Exception e) {
throw new MojoExecutionException("Could not exec java", e);
}
} }
private final String getStartClass() throws MojoExecutionException { private final String getStartClass() throws MojoExecutionException {
...@@ -168,11 +183,6 @@ public class RunMojo extends AbstractMojo { ...@@ -168,11 +183,6 @@ public class RunMojo extends AbstractMojo {
return mainClass; return mainClass;
} }
private ClassLoader getClassLoader() throws MojoExecutionException {
URL[] urls = getClassPathUrls();
return new URLClassLoader(urls);
}
private URL[] getClassPathUrls() throws MojoExecutionException { private URL[] getClassPathUrls() throws MojoExecutionException {
try { try {
List<URL> urls = new ArrayList<URL>(); List<URL> urls = new ArrayList<URL>();
...@@ -223,92 +233,4 @@ public class RunMojo extends AbstractMojo { ...@@ -223,92 +233,4 @@ public class RunMojo extends AbstractMojo {
} }
} }
private void join(ThreadGroup threadGroup) {
boolean hasNonDaemonThreads;
do {
hasNonDaemonThreads = false;
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
for (Thread thread : threads) {
if (thread != null && !thread.isDaemon()) {
try {
hasNonDaemonThreads = true;
thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
while (hasNonDaemonThreads);
}
/**
* Isolated {@link ThreadGroup} to capture uncaught exceptions.
*/
class IsolatedThreadGroup extends ThreadGroup {
private Throwable exception;
public IsolatedThreadGroup(String name) {
super(name);
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!(ex instanceof ThreadDeath)) {
synchronized (this) {
this.exception = (this.exception == null ? ex : this.exception);
}
getLog().warn(ex);
}
}
public synchronized void rethrowUncaughtException() throws MojoExecutionException {
if (this.exception != null) {
throw new MojoExecutionException("An exception occured while running. "
+ this.exception.getMessage(), this.exception);
}
}
}
/**
* Runner used to launch the application.
*/
class LaunchRunner implements Runnable {
private final String startClassName;
private final String[] args;
public LaunchRunner(String startClassName, String... args) {
this.startClassName = startClassName;
this.args = (args != null ? args : new String[] {});
}
@Override
public void run() {
Thread thread = Thread.currentThread();
ClassLoader classLoader = thread.getContextClassLoader();
try {
Class<?> startClass = classLoader.loadClass(this.startClassName);
Method mainMethod = startClass.getMethod("main",
new Class[] { String[].class });
if (!mainMethod.isAccessible()) {
mainMethod.setAccessible(true);
}
mainMethod.invoke(null, new Object[] { this.args });
}
catch (NoSuchMethodException ex) {
Exception wrappedEx = new Exception(
"The specified mainClass doesn't contain a "
+ "main method with appropriate signature.", ex);
thread.getThreadGroup().uncaughtException(thread, wrappedEx);
}
catch (Exception ex) {
thread.getThreadGroup().uncaughtException(thread, ex);
}
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment