+ * CAVEAT: Since JDK 17, you need to set system property this option is necessary to avoid + * # "UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release" + * + * @since 3.1.2 + */ + @Parameter( property = "exec.blockSystemExit", defaultValue = "false" ) + private boolean blockSystemExit; + /** * Execute goal. * @@ -255,6 +272,12 @@ public void execute() IsolatedThreadGroup threadGroup = new IsolatedThreadGroup( mainClass /* name */ ); Thread bootstrapThread = new Thread( threadGroup, new Runnable() { + // TODO: + // Adjust implementation for future JDKs after removal of SecurityManager. + // See https://openjdk.org/jeps/411 for basic information. + // See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to block + // System::exit in post-removal JDKs (still undecided at the time of writing this comment). + @SuppressWarnings( "removal" ) public void run() { int sepIndex = mainClass.indexOf( '/' ); @@ -268,6 +291,8 @@ public void run() { bootClassName = mainClass; } + + SecurityManager originalSecurityManager = System.getSecurityManager(); try { @@ -279,6 +304,10 @@ public void run() lookup.findStatic( bootClass, "main", MethodType.methodType( void.class, String[].class ) ); + if ( blockSystemExit ) + { + System.setSecurityManager( new SystemExitManager( originalSecurityManager ) ); + } mainHandle.invoke( arguments ); } catch ( IllegalAccessException | NoSuchMethodException | NoSuchMethodError e ) @@ -292,10 +321,25 @@ public void run() Throwable exceptionToReport = e.getCause() != null ? e.getCause() : e; Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), exceptionToReport ); } + catch ( SystemExitException systemExitException ) + { + getLog().info( systemExitException.getMessage() ); + if ( systemExitException.getExitCode() != 0 ) + { + throw systemExitException; + } + } catch ( Throwable e ) { // just pass it on Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), e ); } + finally + { + if ( blockSystemExit ) + { + System.setSecurityManager( originalSecurityManager ); + } + } } }, mainClass + ".main()" ); URLClassLoader classLoader = getClassLoader(); diff --git a/src/main/java/org/codehaus/mojo/exec/SystemExitException.java b/src/main/java/org/codehaus/mojo/exec/SystemExitException.java new file mode 100644 index 00000000..5a24f50b --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/SystemExitException.java @@ -0,0 +1,41 @@ +package org.codehaus.mojo.exec; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +/** + * Exception to be thrown by {@link SystemExitManager} when {@link System#exit(int)} is called + * + * @author Alexander Kriegisch + */ +public class SystemExitException extends SecurityException +{ + private final int exitCode; + + public SystemExitException( String s, int exitCode ) + { + super( s ); + this.exitCode = exitCode; + } + + public int getExitCode() + { + return exitCode; + } +} diff --git a/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java new file mode 100644 index 00000000..fc9f5b90 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/SystemExitManager.java @@ -0,0 +1,75 @@ +package org.codehaus.mojo.exec; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +import java.security.Permission; + +/** + * A special security manager (SM) passing on permission checks to the original SM it replaces, except for + * {@link #checkExit(int)} + * + * @author Alexander Kriegisch + */ +public class SystemExitManager extends SecurityManager +{ + private final SecurityManager originalSecurityManager; + + public SystemExitManager( SecurityManager originalSecurityManager ) + { + this.originalSecurityManager = originalSecurityManager; + } + + /** + * Always throws a {@link SystemExitException} when {@link System#exit(int)} is called, instead of terminating the + * JVM. + *
+ * The exception is meant to be handled in the {@code exec:java} goal. On the one hand, this avoids that Java + * code called in process can terminate the JVM and the whole Maven build process with it. On the other hand, the + * exception handler can also differentiate between exit status 0 (OK) and non-0 (error) by inspecting + * {@link SystemExitException#getExitCode()}: + *