Skip to content

Commit

Permalink
[#389] Add option 'blockSystemExit' to 'java' mojo
Browse files Browse the repository at this point in the history
This new option enables users to stop programs called by 'exec:java'
from calling System::exit, terminating not just the mojo but the whole
Maven JVM.

Closes #389.
Relates to #388.
  • Loading branch information
kriegaex committed Nov 9, 2023
1 parent e0afc0b commit 1e10152
Show file tree
Hide file tree
Showing 15 changed files with 456 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions src/it/projects/mexec-gh-389-block-exit-non-zero/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.codehaus.mojo.exec.it</groupId>
<artifactId>parent</artifactId>
<version>0.1</version>
</parent>

<artifactId>mexec-gh-389</artifactId>
<version>0.0.1-SNAPSHOT</version>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>Main</mainClass>
<blockSystemExit>true</blockSystemExit>
<systemProperties>
<systemProperty>
<key>exitBehaviour</key>
<value>system-exit-error</value>
</systemProperty>
</systemProperties>
<arguments>
<argument>one</argument>
<argument>two</argument>
<argument>three</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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" );
}
}
30 changes: 30 additions & 0 deletions src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions src/it/projects/mexec-gh-389-block-exit-zero/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.codehaus.mojo.exec.it</groupId>
<artifactId>parent</artifactId>
<version>0.1</version>
</parent>

<artifactId>mexec-gh-389</artifactId>
<version>0.0.1-SNAPSHOT</version>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>Main</mainClass>
<blockSystemExit>true</blockSystemExit>
<systemProperties>
<systemProperty>
<key>exitBehaviour</key>
<value>system-exit-ok</value>
</systemProperty>
</systemProperties>
<arguments>
<argument>one</argument>
<argument>two</argument>
<argument>three</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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" );
}
}
26 changes: 26 additions & 0 deletions src/it/projects/mexec-gh-389-block-exit-zero/verify.groovy
Original file line number Diff line number Diff line change
@@ -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]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
invoker.goals = clean process-classes
# Cannot not check result, because build terminates unexpectedly
# invoker.buildResult = failure
invoker.debug = false
49 changes: 49 additions & 0 deletions src/it/projects/mexec-gh-389-default-permit-exit/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.codehaus.mojo.exec.it</groupId>
<artifactId>parent</artifactId>
<version>0.1</version>
</parent>

<artifactId>mexec-gh-389</artifactId>
<version>0.0.1-SNAPSHOT</version>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>Main</mainClass>
<!-- This is the default, no need to specify it -->
<!--<blockSystemExit>false</blockSystemExit>-->
<systemProperties>
<systemProperty>
<key>exitBehaviour</key>
<!-- System.exit with any return code will terminate the JVM and the whole Maven process -->
<value>system-exit-ok</value>
</systemProperty>
</systemProperties>
<arguments>
<argument>one</argument>
<argument>two</argument>
<argument>three</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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" );
}
}
24 changes: 24 additions & 0 deletions src/it/projects/mexec-gh-389-default-permit-exit/verify.groovy
Original file line number Diff line number Diff line change
@@ -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]"
44 changes: 44 additions & 0 deletions src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,23 @@ public class ExecJavaMojo
@Parameter
private List<String> 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.
* <p>
* <b>CAVEAT:</b> 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.
*
Expand Down Expand Up @@ -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( '/' );
Expand All @@ -268,6 +291,8 @@ public void run()
{
bootClassName = mainClass;
}

SecurityManager originalSecurityManager = System.getSecurityManager();

try
{
Expand All @@ -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 )
Expand All @@ -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();
Expand Down

0 comments on commit 1e10152

Please sign in to comment.