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