Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ $ grails
grails> run-app
| Grails application running at http://localhost:8080 in environment: development
grails> stop-app
| Shutting down application...
| Application shutdown.
| Stopping application...
| Application stopped.
grails> run-app
| Grails application running at http://localhost:8080 in environment: development
----
Expand Down
19 changes: 8 additions & 11 deletions grails-doc/src/en/ref/Command Line/stop-app.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,27 @@ under the License.
=== Purpose


Stops a running Grails application in an embedded servlet container.
Stops a Grails application that was started with the link:{commandLineRef}run-app.html[run-app] command in the same interactive CLI session.

NOTE: This command will work in development mode only.
NOTE: This command works in development mode only and must be run from the same interactive `grails` session that started the application with `run-app`.


=== Examples


[source,groovy]
----
grails stop-app
grails stop-app --port=9090 --host=mywebsite
$ grails
grails> run-app
grails> stop-app
----


=== Description


Arguments:
When an application is started with `run-app` in the interactive Grails shell, the application runs as an asynchronous Gradle `bootRun` build. The `stop-app` command cancels that build, which shuts down the running application without exiting the CLI. A subsequent `run-app` can then start the application again.

* `port` - Specifies the port which the Grails application is running on (defaults to 8080 for HTTP or 8443 for HTTPS)
* `host` - Specifies the host the Grails application is bound to
This is a CLI only mechanism: it does not require the Spring Boot Actuator shutdown endpoint to be enabled, nor does it rely on JMX.

Supported system properties:

* `server.port` - Same as `port` argument.
* `server.address` - Same as `host` argument.
If no application started by `run-app` is running in the current session, `stop-app` reports that the application is not running.
4 changes: 1 addition & 3 deletions grails-profiles/base/commands/run-app.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ try {
arguments << '--quiet'
}

arguments << '-Dgrails.management.endpoints.shutdown.enabled=true'

arguments.addAll commandLine.remainingArgs

Integer port = flag('port')?.toInteger() ?: config.getProperty('server.port', Integer)
Expand Down Expand Up @@ -106,7 +104,7 @@ try {
addShutdownHook {
if(Boolean.getBoolean("run-app.running")) {
try {
stopApp()
org.grails.cli.gradle.RunningApplicationRegistry.stopAll()
}
catch(e) {
// ignore
Expand Down
101 changes: 11 additions & 90 deletions grails-profiles/base/commands/stop-app.groovy
Original file line number Diff line number Diff line change
@@ -1,101 +1,22 @@
import javax.management.remote.JMXServiceURL
import javax.management.remote.JMXConnectorFactory
import javax.management.ObjectName
import org.grails.io.support.*
import groovy.jmx.GroovyMBean

description("Stops the running Grails application") {
usage "grails stop-app"
synonyms 'stop'
flag name:'port', description:"Specifies the port which the Grails application is running on (defaults to 8080 or 8443 for HTTPS)"
flag name:'host', description:"Specifies the host the Grails application is bound to"
}
System.setProperty("run-app.running", "false")
def getJMXLocalConnectorAddresses = {->
final applicationMainClassName = MainClassFinder.findMainClass()

if(applicationMainClassName) {
try {
final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"
def VirtualMachine = getClass().classLoader.loadClass('com.sun.tools.attach.VirtualMachine')
return VirtualMachine.list()
.findAll {
it.displayName() == applicationMainClassName
}
.collect { desc ->
def vm = VirtualMachine.attach(desc.id())
try {
def connectorAddress = vm.agentProperties.getProperty(CONNECTOR_ADDRESS)
if (connectorAddress == null) {
// Trying to load agent
def agent = [vm.systemProperties.getProperty("java.home"), "lib", "management-agent.jar"].join(File.separator)
vm.loadAgent(agent)

connectorAddress = vm.agentProperties.getProperty(CONNECTOR_ADDRESS)
}
if (connectorAddress) {
return connectorAddress
}
} finally {
vm.detach()
}
}.findAll { it }

}
catch(Throwable e) {
// fallback to REST request if JMX not available
}
}
}

System.setProperty("run-app.running", "false")

def addresses = getJMXLocalConnectorAddresses()
if(addresses) {
JMXServiceURL url = new JMXServiceURL(addresses[0])
def connector = JMXConnectorFactory.connect(url)

try {
def server = connector.MBeanServerConnection
console.updateStatus "Stopping application..."

def objectName = server.queryNames(null,null).find { it.canonicalName.contains('name=shutdownEndpoint,type=Endpoint') }
def mbean = new GroovyMBean(server, objectName)
console.addStatus "Shutting down application..."
mbean.shutdown()
console.addStatus "Application shutdown."
return true
if (org.grails.cli.gradle.RunningApplicationRegistry.stopAll()) {
if (org.grails.cli.gradle.RunningApplicationRegistry.awaitStop(30000)) {
console.updateStatus "Application stopped."
}
catch(e) {
console.addStatus "Application not found via JMX, attempting remote shutdown."
else {
console.updateStatus "Application shutdown requested; it may still be stopping."
}
finally {
connector.close()
}
}


Integer port = flag('port')?.toInteger() ?: config.getProperty('server.port', Integer) ?: 8080
String host = flag('host') ?: config.getProperty('server.address', String) ?: "localhost"
String contextPath = config.getProperty('server.context-path') ?: config.getProperty('server.contextPath') ?: ""
String managementPath = config.getProperty('management.endpoints.web.base-path') ?: config.getProperty('management.endpoints.web.basePath') ?: "/actuator"
console.updateStatus "Shutting down application..."
def url = new URL("http://$host:${port}${contextPath}${managementPath}/shutdown")
try {
def connection = url.openConnection()
connection.setRequestMethod("POST")
connection.doOutput = true
connection.connect()
console.updateStatus connection.content.text
while(isServerAvailable(host, port)) {
sleep 100
}
console.updateStatus "Application shutdown."
return true

return true
}
catch (e) {
console.error "Application not running.", e
return false
else {
console.error "Application not running."
return false
}



Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ class GradleInvoker {
Object invokeMethod(String name, Object args) {
Object[] argArray = (Object[]) args

GradleUtil.runBuildWithConsoleOutput(executionContext) { BuildLauncher buildLauncher ->
// Track only the long running application task launched by run-app so that
// stop-app can cancel it; this excludes transient builds such as compile or test
String taskName = name.split(' ')[0]
boolean trackForStop = taskName == 'bootRun' || taskName.endsWith(':bootRun')

GradleUtil.runBuildWithConsoleOutput(executionContext, trackForStop) { BuildLauncher buildLauncher ->
buildLauncher.forTasks(name.split(' '))
List<String> arguments = []
arguments << "-Dgrails.env=${Environment.current.name}".toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import groovy.transform.stc.SimpleType
import org.gradle.tooling.BuildAction
import org.gradle.tooling.BuildActionExecuter
import org.gradle.tooling.BuildLauncher
import org.gradle.tooling.CancellationTokenSource
import org.gradle.tooling.GradleConnector
import org.gradle.tooling.LongRunningOperation
import org.gradle.tooling.ProjectConnection
import org.gradle.tooling.internal.consumer.DefaultCancellationTokenSource

import grails.build.logging.GrailsConsole
import grails.io.support.SystemOutErrCapturer
Expand Down Expand Up @@ -95,12 +95,28 @@ class GradleUtil {

static void runBuildWithConsoleOutput(ExecutionContext context,
@ClosureParams(value = SimpleType, options = 'org.gradle.tooling.BuildLauncher') Closure<?> buildLauncherCustomizationClosure) {
// workaround for GROOVY-7211, static type checking problem when default parameters are used
runBuildWithConsoleOutput(context, false, buildLauncherCustomizationClosure)
}

static void runBuildWithConsoleOutput(ExecutionContext context, boolean trackForStop,
@ClosureParams(value = SimpleType, options = 'org.gradle.tooling.BuildLauncher') Closure<?> buildLauncherCustomizationClosure) {
withProjectConnection(context.getBaseDir(), DEFAULT_SUPPRESS_OUTPUT) { ProjectConnection projectConnection ->
BuildLauncher launcher = projectConnection.newBuild()
setupConsoleOutput(context, launcher)
wireCancellationSupport(context, launcher)
buildLauncherCustomizationClosure.call(launcher)
launcher.run()
CancellationTokenSource cancellationTokenSource = wireCancellationSupport(context, launcher)
if (trackForStop) {
RunningApplicationRegistry.register(cancellationTokenSource)
}
try {
buildLauncherCustomizationClosure.call(launcher)
launcher.run()
}
finally {
if (trackForStop) {
RunningApplicationRegistry.deregister(cancellationTokenSource)
}
}
}
}

Expand Down Expand Up @@ -136,11 +152,12 @@ class GradleUtil {
return buildActionExecuter.run()
}

static wireCancellationSupport(ExecutionContext context, BuildLauncher buildLauncher) {
DefaultCancellationTokenSource cancellationTokenSource = new DefaultCancellationTokenSource()
static CancellationTokenSource wireCancellationSupport(ExecutionContext context, BuildLauncher buildLauncher) {
CancellationTokenSource cancellationTokenSource = GradleConnector.newCancellationTokenSource()
buildLauncher.withCancellationToken(cancellationTokenSource.token())
context.addCancelledListener({
cancellationTokenSource.cancel()
})
return cancellationTokenSource
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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.cli.gradle

import java.util.concurrent.ConcurrentHashMap

import groovy.transform.CompileStatic

import org.gradle.tooling.CancellationTokenSource

import grails.build.logging.GrailsConsole

/**
* Tracks the {@link CancellationTokenSource} of Grails applications started by the
* CLI via the {@code run-app} command (an asynchronous Gradle {@code bootRun} build).
*
* <p>This allows the {@code stop-app} command to cancel the underlying Gradle build
* without exiting the interactive CLI, providing a CLI only shutdown mechanism that
* does not rely on the Spring Boot Actuator shutdown endpoint or JMX.</p>
*
* <p>A {@code run-app} build registers its token before the build runs and removes it
* when the build finishes (whether it completes normally, fails, or is cancelled).
* {@link #stopAll()} only requests cancellation; it never removes tokens directly so
* that the build remains responsible for its own lifecycle.</p>
*
* @author Apache Grails Team
* @since 7.0.0
*/
@CompileStatic
class RunningApplicationRegistry {

private static final Set<CancellationTokenSource> RUNNING = ConcurrentHashMap.newKeySet()

// Monitor used to wake up awaitStop() the moment the last running build deregisters,
// instead of polling. Only deregister() can empty RUNNING, so only it needs to notify.
private static final Object MONITOR = new Object()

private RunningApplicationRegistry() {
}

/**
* Registers the cancellation token source of a running application.
*
* @param tokenSource the token source backing the running build
*/
static void register(CancellationTokenSource tokenSource) {
if (tokenSource != null) {
RUNNING.add(tokenSource)
}
}

/**
* Removes a previously registered cancellation token source. This should be called
* by the build once it has finished, regardless of how it terminated.
*
* @param tokenSource the token source to remove
*/
static void deregister(CancellationTokenSource tokenSource) {
if (tokenSource != null) {
RUNNING.remove(tokenSource)
synchronized (MONITOR) {
MONITOR.notifyAll()
}
}
}

/**
* @return {@code true} if at least one application started via {@code run-app} is running
*/
static boolean isApplicationRunning() {
!RUNNING.isEmpty()
}

/**
* Requests cancellation of every running application. The registered token sources are
* not removed here; each running build removes its own token when it terminates.
*
* @return {@code true} if at least one running application was found and cancellation requested
*/
static boolean stopAll() {
if (RUNNING.isEmpty()) {
return false
}
// Snapshot to avoid surprises if a build deregisters concurrently while we iterate
List<CancellationTokenSource> tokenSources = new ArrayList<>(RUNNING)
for (CancellationTokenSource tokenSource : tokenSources) {
try {
tokenSource.cancel()
}
catch (Throwable e) {
GrailsConsole.getInstance().verbose("Failed to request cancellation of a running application: ${e.message}")
}
}
return true
}

/**
* Waits up to the given timeout for all running applications to terminate, i.e. for the
* cancelled builds to finish tearing down and deregister their token sources.
*
* @param timeoutMillis the maximum time to wait in milliseconds
* @return {@code true} if all applications stopped within the timeout, {@code false} otherwise
*/
static boolean awaitStop(long timeoutMillis) {
long deadline = System.currentTimeMillis() + timeoutMillis
synchronized (MONITOR) {
while (!RUNNING.isEmpty()) {
long remaining = deadline - System.currentTimeMillis()
if (remaining <= 0) {
return false
}
try {
MONITOR.wait(remaining)
}
catch (InterruptedException e) {
Thread.currentThread().interrupt()
return RUNNING.isEmpty()
}
}
return true
}
}
Comment thread
jamesfredley marked this conversation as resolved.
}
Loading
Loading