Skip to content

Fix stop-app in the interactive CLI without Actuator or JMX#15697

Closed
jamesfredley wants to merge 1 commit into
apache:7.0.xfrom
jamesfredley:fix/stop-app-cli
Closed

Fix stop-app in the interactive CLI without Actuator or JMX#15697
jamesfredley wants to merge 1 commit into
apache:7.0.xfrom
jamesfredley:fix/stop-app-cli

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

Fixes #13695

Problem

stop-app in the interactive Grails shell does not work. The base profile stop-app command tries two mechanisms, both of which fail on a modern setup:

  1. JMX via com.sun.tools.attach.VirtualMachine, loading management-agent.jar - that jar was removed in Java 9+, and the code uses javax.*. This silently fails on JDK 17/21.
  2. HTTP fallback that POSTs to the Spring Boot Actuator /actuator/shutdown endpoint - disabled by default, so it throws FileNotFoundException (HTTP 404), reporting "Application not running" even though it is.

Enabling the Actuator shutdown endpoint is not an acceptable fix: it is intrusive and affects production for the sake of a development-time stop-app.

Approach (CLI only, no Actuator, no JMX)

In the interactive shell, run-app starts the application as an asynchronous Gradle bootRun build. That build already has a Gradle Tooling API CancellationTokenSource - it is what CTRL-C uses to stop the app. The problem is that the token was only reachable through the run-app command's ExecutionContext, so stop-app (a separate ExecutionContext in the same CLI JVM) had no handle to it.

This PR introduces a small shared registry so stop-app can cancel the running build directly, without POOL.shutdownNow() (which would kill the executor and block re-running) and without exiting the CLI.

Changes

  • New RunningApplicationRegistry (grails-shell-cli) - tracks the CancellationTokenSource of running run-app builds. stopAll() only requests cancellation; the build deregisters its own token when it finishes. awaitStop(timeout) waits for the build to tear down.
  • GradleUtil - wireCancellationSupport now returns the token source, created via the public GradleConnector.newCancellationTokenSource() instead of the internal DefaultCancellationTokenSource. A new runBuildWithConsoleOutput(context, trackForStop, closure) overload registers/deregisters the token around the build.
  • GradleInvoker - tracks only the bootRun task (matches bootRun or :bootRun), so transient builds (compile, test, console) are not affected.
  • stop-app.groovy - rewritten to cancel via the registry and wait for shutdown; obsolete port/host flags removed.
  • run-app.groovy - no longer passes -Dgrails.management.endpoints.shutdown.enabled=true (only existed to enable the Actuator endpoint); the JVM shutdown hook now calls RunningApplicationRegistry.stopAll() directly instead of re-invoking the stop-app command.
  • Docs - updated stop-app reference and the getting-started running/debugging guide.

Tests

  • RunningApplicationRegistrySpec covers register/deregister, stopAll requesting cancellation without removing tokens, resilience when a token throws, and awaitStop success/timeout.
  • ./gradlew :grails-shell-cli:test --tests "org.grails.cli.gradle.RunningApplicationRegistrySpec" passes.

Manual verification still recommended

The forked application JVM is torn down by Gradle when the bootRun build is cancelled (the same path CTRL-C uses). A manual run of grails run-app -> grails stop-app -> grails run-app on JDK 21 (Windows and Linux) is recommended before merge to confirm no orphaned process and that the port is freed.

Notes

  • This is a draft for review of the approach on 7.0.x. The linked issue is milestoned for 8.0.0-M3; opening here first per discussion.

The base profile stop-app command relied on JMX (via
com.sun.tools.attach.VirtualMachine and the management-agent.jar that was
removed in Java 9+) with an HTTP fallback to the Spring Boot Actuator
/actuator/shutdown endpoint. The JMX path is broken on modern JDKs and the
Actuator endpoint is disabled by default, so stop-app failed with a
FileNotFoundException against /actuator/shutdown even though the application
was running.

In the interactive shell, run-app starts the application as an asynchronous
Gradle bootRun build whose cancellation token was only reachable through the
run-app command's ExecutionContext. This adds a RunningApplicationRegistry
that tracks the running build's CancellationTokenSource so that stop-app can
cancel it directly - the same mechanism CTRL-C already uses - without
shutting down the CLI.

- Add RunningApplicationRegistry to track running bootRun builds.
- GradleUtil.wireCancellationSupport now returns the token source (created
  via the public GradleConnector.newCancellationTokenSource() instead of the
  internal DefaultCancellationTokenSource) and a new runBuildWithConsoleOutput
  overload registers and deregisters it.
- GradleInvoker tracks only the bootRun task for stop support.
- Rewrite stop-app to cancel the running build via the registry and wait for
  it to stop; remove the obsolete port/host flags.
- run-app no longer enables the Actuator shutdown endpoint and its shutdown
  hook cancels via the registry directly.
- Update the reference and getting-started docs.

Fixes apache#13695

Assisted-by: opencode:claude-opus-4-8
@jamesfredley jamesfredley marked this pull request as ready for review May 29, 2026 04:31
Copilot AI review requested due to automatic review settings May 29, 2026 04:31
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Superseded by #15698 (branch pushed to apache/grails-core instead of a fork).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix stop-app in grails cli

2 participants