This is an automated email from the ASF dual-hosted git repository.

codeconsole pushed a commit to branch fix/stop-app-cli
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/fix/stop-app-cli by this push:
     new c5265007e8 Report a deliberate stop-app as a successful bootRun build
c5265007e8 is described below

commit c5265007e85c09f8dd0218690f0930daa8a5f3a8
Author: Scott Murphy Heiberg <[email protected]>
AuthorDate: Thu Jun 18 16:20:49 2026 -0700

    Report a deliberate stop-app as a successful bootRun build
    
    stop-app (and Ctrl+C) terminate the forked application gracefully via 
SIGTERM/SIGINT, so the JVM runs its shutdown hooks and exits with a signal 
status code (143 for SIGTERM, 130 for SIGINT). JavaExec treats any non-zero 
exit as a build failure, so a normal, graceful stop was reported as BUILD 
FAILED.
    
    Configure bootRun to tolerate those signal-termination codes via a 
BootRunExitCodeVerifier doLast action - still failing the build for any other 
non-zero exit such as a genuine startup error - so a deliberate stop is 
reported as BUILD SUCCESSFUL. run-app reports the clean shutdown now that the 
build succeeds.
---
 .../plugin/core/BootRunExitCodeVerifier.groovy     | 52 ++++++++++++++++++++++
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   |  4 ++
 .../plugin/core/BootRunExitCodeVerifierSpec.groovy | 51 +++++++++++++++++++++
 grails-profiles/base/commands/run-app.groovy       |  9 +++-
 4 files changed, 114 insertions(+), 2 deletions(-)

diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifier.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifier.groovy
new file mode 100644
index 0000000000..512f86751b
--- /dev/null
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifier.groovy
@@ -0,0 +1,52 @@
+/*
+ *  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
+ *
+ *    https://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.grails.gradle.plugin.core
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.Action
+import org.gradle.api.GradleException
+import org.gradle.api.Task
+import org.gradle.api.tasks.JavaExec
+
+/**
+ * A {@code bootRun} {@code doLast} action that treats a deliberate stop as a 
successful build.
+ *
+ * <p>{@code stop-app} and {@code Ctrl+C} terminate the forked application 
gracefully (SIGTERM /
+ * SIGINT), so it exits 143 / 130. {@link JavaExec} would otherwise report 
that non-zero exit as
+ * {@code BUILD FAILED}; this tolerates the signal-termination codes while 
still failing for any
+ * other non-zero exit. Used with {@code ignoreExitValue = true} on the 
task.</p>
+ */
+@CompileStatic
+class BootRunExitCodeVerifier implements Action<Task> {
+
+    // 0 = clean exit, 143 = 128 + SIGTERM (stop-app), 130 = 128 + SIGINT 
(Ctrl+C)
+    private static final Set<Integer> EXPECTED_EXIT_CODES = [0, 143, 130] as 
Set<Integer>
+
+    @Override
+    void execute(Task task) {
+        verify(((JavaExec) task).executionResult.get().exitValue)
+    }
+
+    void verify(int exitValue) {
+        if (!EXPECTED_EXIT_CODES.contains(exitValue)) {
+            throw new GradleException("Application exited abnormally (exit 
code ${exitValue})".toString())
+        }
+    }
+}
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
index c0e39638eb..8b913e19c7 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
@@ -612,6 +612,10 @@ class GrailsGradlePlugin extends GroovyPlugin {
                 // force the build directory provider during configuration. 
The forked application
                 // reads this location so stop-app can locate and terminate it.
                 task.jvmArgumentProviders.add(new 
RunAppPidFileProvider(CLI_PID_FILE_PROPERTY, pidFile))
+
+                // Report a deliberate stop as a successful build (see 
BootRunExitCodeVerifier).
+                task.ignoreExitValue = true
+                task.doLast(new BootRunExitCodeVerifier())
             }
         }
     }
diff --git 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifierSpec.groovy
 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifierSpec.groovy
new file mode 100644
index 0000000000..132cdfae14
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BootRunExitCodeVerifierSpec.groovy
@@ -0,0 +1,51 @@
+/*
+ *  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
+ *
+ *    https://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.grails.gradle.plugin.core
+
+import org.gradle.api.GradleException
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class BootRunExitCodeVerifierSpec extends Specification {
+
+    @Unroll
+    void "exit code #code from a deliberate stop is tolerated as a successful 
build"() {
+        when: "an exit code produced by a graceful stop is verified"
+        new BootRunExitCodeVerifier().verify(code)
+
+        then: "the build is not failed"
+        noExceptionThrown()
+
+        where: "the application exited cleanly (0), or on SIGTERM (143) or 
SIGINT (130)"
+        code << [0, 143, 130]
+    }
+
+    @Unroll
+    void "exit code #code from an abnormal exit fails the build"() {
+        when: "any other non-zero exit code is verified"
+        new BootRunExitCodeVerifier().verify(code)
+
+        then: "the build is failed and the message names the exit code"
+        GradleException e = thrown()
+        e.message.contains(code.toString())
+
+        where: "a real startup error (1), a force-kill (137) or any other 
non-zero code"
+        code << [1, 2, 137, 255]
+    }
+}
diff --git a/grails-profiles/base/commands/run-app.groovy 
b/grails-profiles/base/commands/run-app.groovy
index 0ba967ce46..e06afe0cad 100644
--- a/grails-profiles/base/commands/run-app.groovy
+++ b/grails-profiles/base/commands/run-app.groovy
@@ -91,6 +91,11 @@ try {
         else {
             gradle."bootRun"(*arguments)
         }
+        // A deliberate stop is a successful build, so bootRun returns here 
instead of throwing.
+        
if(org.grails.cli.gradle.RunningApplicationProcess.isStopRequested(buildDir)) {
+            console.updateStatus("Application stopped")
+            return true
+        }
     }
     else {
         def future
@@ -137,8 +142,8 @@ catch(org.gradle.tooling.BuildCancelledException e) {
     return true
 }
 catch(Throwable e) {
-    // A deliberate stop-app terminates the bootRun process, which surfaces 
here as a build
-    // failure; report it as a clean shutdown rather than a startup failure.
+    // Fallback for a force-killed app (did not exit on SIGTERM in time): a 
requested stop is a clean
+    // shutdown, not a startup failure. A normal stop succeeds and is handled 
above.
     
if(org.grails.cli.gradle.RunningApplicationProcess.isStopRequested(buildDir)) {
         console.updateStatus("Application stopped")
         return true

Reply via email to