diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties b/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties new file mode 100644 index 00000000..b2abeb1c --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/invoker.properties @@ -0,0 +1,6 @@ +invoker.goals = clean process-classes +invoker.buildResult = failure +invoker.debug = false +# Since JDK 17, this option is necessary to avoid +# "UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release" +invoker.mavenOpts = -Djava.security.manager=allow diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml b/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml new file mode 100644 index 00000000..39de6618 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + true + + + exitBehaviour + system-exit-error + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java b/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} diff --git a/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy b/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy new file mode 100644 index 00000000..b9d64f4e --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy @@ -0,0 +1,30 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Find "System::exit was called" line index +def infoMessageLineNumber = buildLogLines.indexOf("[INFO] System::exit was called with return code 123") +assert infoMessageLineNumber > 0 +// Verify that preceding line is program output +assert buildLogLines[infoMessageLineNumber - 1] == "[one, two, three]" +// Verify that subsequent lines contain the beginning of the thrown SystemExitException stack trace +assert buildLogLines[infoMessageLineNumber + 1].startsWith("[WARNING]") +assert buildLogLines[infoMessageLineNumber + 2].contains("SystemExitException: System::exit was called with return code 123") +assert buildLogLines[infoMessageLineNumber + 3].contains("SystemExitManager.checkExit (SystemExitManager.java") diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties b/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties new file mode 100644 index 00000000..d95c8ef2 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/invoker.properties @@ -0,0 +1,6 @@ +invoker.goals = clean process-classes +invoker.buildResult = success +invoker.debug = false +# Since JDK 17, this option is necessary to avoid +# "UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release" +invoker.mavenOpts = -Djava.security.manager=allow diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml b/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml new file mode 100644 index 00000000..67e32700 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + true + + + exitBehaviour + system-exit-ok + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java b/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} diff --git a/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy b/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy new file mode 100644 index 00000000..1031a79f --- /dev/null +++ b/src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy @@ -0,0 +1,26 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Find "System::exit was called" line index +def infoMessageLineNumber = buildLogLines.indexOf("[INFO] System::exit was called with return code 0") +assert infoMessageLineNumber > 0 +// Verify that preceding line is program output +assert buildLogLines[infoMessageLineNumber - 1] == "[one, two, three]" diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties b/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties new file mode 100644 index 00000000..4c2f7483 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/invoker.properties @@ -0,0 +1,4 @@ +invoker.goals = clean process-classes +# Cannot not check result, because build terminates unexpectedly +# invoker.buildResult = failure +invoker.debug = false diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml b/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml new file mode 100644 index 00000000..b4f6ba90 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + parent + 0.1 + + + mexec-gh-389 + 0.0.1-SNAPSHOT + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + + process-classes + + java + + + Main + + + + + exitBehaviour + + system-exit-ok + + + + one + two + three + + + + + + + + diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java b/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java new file mode 100644 index 00000000..ba642258 --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/src/main/java/Main.java @@ -0,0 +1,19 @@ +import java.util.Arrays; + +public class Main +{ + public static void main( String[] args ) + { + System.out.println( Arrays.toString( args ) ); + switch ( System.getProperty( "exitBehaviour", "ok" ) ) + { + case "throw-exception": + throw new RuntimeException( "uh-oh" ); + case "system-exit-ok": + System.exit( 0 ); + case "system-exit-error": + System.exit( 123 ); + } + System.out.println( "OK" ); + } +} diff --git a/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy b/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy new file mode 100644 index 00000000..11bab46a --- /dev/null +++ b/src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy @@ -0,0 +1,24 @@ +/* + * 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. + */ + +def buildLogLines = new File( basedir, "build.log" ).readLines() + +// Second-last line is the last line the called program prints before exiting the JVM with System.exit. +// Last line is "Running post-build script: ...", i.e. we need to disregard it. +assert buildLogLines[-2] == "[one, two, three]" diff --git a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java index f9444883..197b367a 100644 --- a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java +++ b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -206,6 +206,23 @@ public class ExecJavaMojo @Parameter private List classpathFilenameExclusions; + /** + * Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build) + * by calling {@link System#exit(int)}. When active, a special security manager will intercept those calls. In case + * of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. Otherwise, it will + * throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself had exited with an + * exception. This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users + * had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no + * longer necessary. + *

+ * 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()}: + *

+ * + * @param status the exit status + */ + @Override + public void checkExit( int status ) + { + throw new SystemExitException( "System::exit was called with return code " + status, status ); + } + + @Override + public void checkPermission( Permission perm ) + { + if ( originalSecurityManager != null ) + { + originalSecurityManager.checkPermission( perm ); + } + } + +}