codeconsole commented on code in PR #16094:
URL: https://github.com/apache/grails-core/pull/16094#discussion_r3792529535


##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/events/ConfigurableApplicationContextEventPublisher.groovy:
##########
@@ -32,14 +34,26 @@ import 
org.springframework.context.ConfigurableApplicationContext
  * @since 6.0
  */
 @CompileStatic
-class ConfigurableApplicationContextEventPublisher implements 
ConfigurableApplicationEventPublisher {
+class ConfigurableApplicationContextEventPublisher implements 
ConfigurableApplicationEventPublisher, ApplicationContextAware {
 
-    final ConfigurableApplicationContext applicationContext
+    ConfigurableApplicationContext applicationContext
+
+    /**
+     * Takes the context from the container. A bean definition built this way 
holds no
+     * already-constructed object, which is what allows it to be processed 
ahead of time.
+     */
+    ConfigurableApplicationContextEventPublisher() {
+    }
 
     
ConfigurableApplicationContextEventPublisher(ConfigurableApplicationContext 
applicationContext) {
         this.applicationContext = applicationContext
     }
 
+    @Override
+    void setApplicationContext(ApplicationContext applicationContext) {

Review Comment:
   `416096af49`.
   
   - `setApplicationContext` narrows with `instanceof` and leaves the field 
unset otherwise.
   - `addApplicationListener` and both `publishEvent` overloads go through 
`context()`, which throws `IllegalStateException` naming the plain-bean-factory 
case instead of an NPE from inside GORM.
   - The field is `volatile`.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/aot/ExtractApplicationTask.groovy:
##########
@@ -0,0 +1,88 @@
+/*
+ *  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.aot
+
+import javax.inject.Inject
+
+import groovy.transform.CompileStatic
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+
+/**
+ * Unpacks an executable jar into the form an AOT cache is read against.
+ *
+ * <p>An executable jar loads its dependencies through a nested-jar class 
loader; the extracted form
+ * loads them as ordinary jars on the classpath. A cache records the class 
path it saw, so only the
+ * second is a layout a later run can reuse one against.</p>
+ *
+ * <p>The work is done through injected services rather than through the 
project, so that nothing is
+ * reached at execution time that the configuration cache forbids.</p>
+ *
+ * @since 8.0
+ */
+@CacheableTask
+@CompileStatic
+abstract class ExtractApplicationTask extends DefaultTask {
+
+    /** The archive to unpack, which has to be the one the cache will be 
trained against. */
+    @InputFile
+    @PathSensitive(PathSensitivity.RELATIVE)
+    abstract RegularFileProperty getArchiveFile()
+
+    /**
+     * The java to unpack with. Its {@code jarmode} does the extracting, so it 
is the JDK the
+     * application was built for rather than whichever one is running Gradle.
+     */
+    @Input
+    abstract Property<String> getJavaExecutable()

Review Comment:
   `a3d5a48b39`. `@Input Property<String> javaExecutable` → `@Nested 
Property<JavaLauncher> javaLauncher`, as in `GroovyPageForkCompileTask`. The 
plugin sets it with the launcher provider, so reading the project still 
provisions no JDK. Added `ExtractApplicationTaskSpec`.
   
   `TrainAotCacheTask` keeps its executable path: it is 
`@DisableCachingByDefault`, and its spec substitutes a shell script for the 
JVM, which a `JavaLauncher` cannot express.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/aot/TrainAotCacheTask.groovy:
##########
@@ -0,0 +1,376 @@
+/*
+ *  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.aot
+
+import java.security.MessageDigest
+import java.time.Duration
+
+import groovy.transform.CompileStatic
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Runs the application once so the JDK can write down what starting it needs.
+ *
+ * <p>The run is the point. A cache records the classes loaded and linked, and 
the profiles of the
+ * methods that ran, so the next start reads them rather than working them out 
again -- which means
+ * what the next start is <em>fast at</em> is whatever this run did. A run 
that only refreshes the
+ * context leaves every request path to be worked out on the day.</p>
+ *
+ * <p>So the application is started, asked for the pages an application is 
asked for, and then asked
+ * to stop. It has to stop of its own accord: the cache is written as the JVM 
exits, and a run that is
+ * killed writes nothing.</p>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+@DisableCachingByDefault(because = 'Runs the application and records what it 
did, which is not reproducible')
+abstract class TrainAotCacheTask extends DefaultTask {
+
+    /**
+     * The extracted application: the cache is only usable against the layout 
it was trained on.
+     *
+     * <p>Compared by what is in it and where each file sits within it, not by 
where the directory
+     * itself is. Without saying so, Gradle takes the absolute path to be part 
of the input and
+     * refuses to validate the task at all -- and a build that did run would 
key its result to a
+     * path, so the same application checked out somewhere else, or built on 
CI, would agree about
+     * everything and still share nothing.</p>
+     */
+    @InputDirectory
+    @PathSensitive(PathSensitivity.RELATIVE)
+    abstract DirectoryProperty getApplicationDirectory()
+
+    @Input
+    abstract Property<String> getArchiveFileName()
+
+    @OutputFile
+    abstract RegularFileProperty getCacheFile()
+
+    @Input
+    abstract Property<String> getJavaExecutable()
+
+    /** The version of the JDK above, which is the JDK the cache will only 
ever be readable by. */
+    @Input
+    abstract Property<String> getJavaVersion()
+
+    @Input
+    abstract Property<String> getJavaVendor()
+
+    /** Given to the training run, and to be given to every run that reads the 
cache. */
+    @Input
+    abstract ListProperty<String> getJvmArguments()
+
+    /** The paths to ask for, so their methods are profiled rather than met 
for the first time later. */
+    @Input
+    abstract ListProperty<String> getPaths()
+
+    @Input
+    abstract Property<Integer> getPort()
+
+    @Input
+    abstract Property<Integer> getStartTimeoutSeconds()
+
+    /** Written beside the cache, so what the cache was made from can be 
checked before it is used. */
+    @OutputFile
+    abstract RegularFileProperty getMetadataFile()
+
+    /** The first JDK that can write one. Before it, {@code 
-XX:AOTCacheOutput} is not an option. */
+    private static final int MINIMUM_JAVA_VERSION = 25
+
+    @TaskAction
+    void train() {
+        refuseWhereTheRunCannotBeAskedToStop()
+        refuseWhereTheJdkCannotWriteACache()
+        File directory = applicationDirectory.get().asFile
+        File cache = cacheFile.get().asFile
+        cache.delete()
+
+        List<String> command = []
+        command << javaExecutable.get()
+        command << "-XX:AOTCacheOutput=${cache.absolutePath}".toString()
+        command.addAll(jvmArguments.get())
+        command << '-jar' << archiveFileName.get()
+        command << "--server.port=${port.get()}".toString()
+
+        File output = new File(temporaryDir, 'training.log')
+        ProcessBuilder builder = new ProcessBuilder(command)
+                .directory(directory)
+                .redirectErrorStream(true)
+                .redirectOutput(output)
+        withoutEmptyVariables(builder.environment())
+        Process process = builder.start()
+        try {
+            awaitStarted(process, output)
+            exercise()
+        }
+        finally {
+            stop(process)
+        }
+        if (!cache.isFile()) {
+            throw new GradleException("The training run wrote no cache. What 
it printed is in ${output}")
+        }
+        describe(cache, new File(directory, archiveFileName.get()))
+        logger.lifecycle('Trained {} ({} MB) over {} paths',
+                cache.name, (cache.length() / (1024 * 1024)) as long, 
paths.get().size())
+    }
+
+    /**
+     * Drops the variables that are present but empty from what the run will 
inherit.
+     *
+     * <p>The run inherits the daemon's environment, and the daemon's is not 
the one the build was
+     * started from. A daemon that once ran a build with a variable set keeps 
the name afterwards
+     * and empties the value, so a later build started from a shell that never 
mentioned it hands
+     * the run {@code SOME_VARIABLE=""} all the same.</p>
+     *
+     * <p>Spring Boot binds an environment variable over the application's own 
configuration, and
+     * relaxed binding means {@code GRAILS_MONGODB_URL} is {@code 
grails.mongodb.url}. So an empty
+     * leftover replaced a configured value with nothing, and the training run 
failed on a property
+     * the application had set correctly -- reporting it against a name nobody 
had typed, in a
+     * build that had run cleanly minutes earlier from a different shell.</p>
+     *
+     * <p>An empty variable says nothing that an absent one does not, so it is 
not passed on. What
+     * the build was actually given, empty or not, still arrives: this drops 
only what the daemon
+     * kept after the build that set it had finished.</p>
+     */
+    private static void withoutEmptyVariables(Map<String, String> environment) 
{
+        environment.entrySet().removeIf { Map.Entry<String, String> variable ->
+            !variable.value
+        }
+    }
+
+    /**
+     * Refuses to start a run that could not be ended properly, before it is 
started.
+     *
+     * <p>The cache is written as the training JVM exits normally, so the run 
has to be asked to
+     * stop rather than killed. {@link Process#destroy()} asks on POSIX and 
kills on Windows, where
+     * it is {@code TerminateProcess} and no shutdown runs -- so on Windows 
the run would be
+     * exercised in full, killed, and leave no cache, and the build would fail 
at the end saying the
+     * cache was not written rather than saying why it could not be.</p>
+     */
+    private static void refuseWhereTheRunCannotBeAskedToStop() {
+        String os = System.getProperty('os.name', '')
+        if (os.toLowerCase(Locale.ROOT).contains('win')) {
+            throw new GradleException('Training an AOT cache needs the 
training run to be asked to ' +
+                    'stop, and on Windows a child process can only be killed 
-- which writes no ' +
+                    'cache. Train on Linux or macOS, or set 
grails.aotCache.enabled to false.')
+        }
+    }
+
+    /**
+     * Refuses on a JDK that has no cache to write, before the run is started.
+     *
+     * <p>{@code -XX:AOTCacheOutput} arrived in JDK 25. An earlier JDK does 
not recognise it and
+     * stops immediately, so the run would be started, fail at once, and be 
reported as having
+     * ended before it started serving -- which reads as a broken application 
rather than as the
+     * wrong JDK, and sends whoever is reading the build into a log of the 
application's own
+     * start-up that never happened.</p>
+     *
+     * <p>Asked of the JDK the training will run on, which is the project's 
toolchain rather than
+     * whichever one is running Gradle.</p>
+     */
+    private void refuseWhereTheJdkCannotWriteACache() {
+        String version = javaVersion.get()
+        Integer major = majorVersionOf(version)
+        if (major != null && major < MINIMUM_JAVA_VERSION) {
+            throw new GradleException("Training an AOT cache needs Java 
${MINIMUM_JAVA_VERSION} or " +
+                    "later, and the toolchain this would train on is 
${version}. Point the project " +
+                    'toolchain at a newer JDK, or set grails.aotCache.enabled 
to false.')
+        }
+    }
+
+    /**
+     * The feature version of a runtime version string, or {@code null} where 
it does not begin with
+     * one. Unreadable rather than old: a version this cannot parse is left to 
the run to answer for,
+     * since refusing over it would fail a build on a JDK that may be 
perfectly capable.
+     */
+    private static Integer majorVersionOf(String version) {
+        int digits = 0
+        while (digits < version.length() && 
Character.isDigit(version.charAt(digits))) {
+            digits++
+        }
+        digits > 0 ? Integer.valueOf(version.substring(0, digits)) : null
+    }
+
+    /**
+     * Writes down what the cache was made from.
+     *
+     * <p>A cache is read only by the JDK build that wrote it, against the 
archive it was trained on,
+     * with the arguments it was trained with. A JVM given one made from 
anything else declines it and
+     * starts as it would have anyway -- so what is lost is the speed, 
silently, and this is what
+     * tells a deployment which of those it has.</p>
+     */
+    private void describe(File cache, File archive) {
+        Properties properties = new Properties()
+        properties.setProperty('cache.file', cache.name)
+        properties.setProperty('cache.bytes', String.valueOf(cache.length()))
+        properties.setProperty('application.archive', archive.name)
+        properties.setProperty('application.sha256', sha256(archive))
+        properties.setProperty('training.arguments', jvmArguments.get().join(' 
'))
+        properties.setProperty('training.paths', paths.get().join(' '))
+        properties.setProperty('java.vendor', javaVendor.get())
+        properties.setProperty('java.runtime.version', javaVersion.get())
+        properties.setProperty('os.name', System.getProperty('os.name', ''))
+        properties.setProperty('os.arch', System.getProperty('os.arch', ''))
+        metadataFile.get().asFile.withOutputStream { OutputStream out ->
+            properties.store(out, 'What this AOT cache was trained from')
+        }
+    }
+
+    private static String sha256(File file) {
+        MessageDigest digest = MessageDigest.getInstance('SHA-256')
+        file.withInputStream { InputStream input ->
+            byte[] buffer = new byte[8192]
+            int read
+            while ((read = input.read(buffer)) != -1) {
+                digest.update(buffer, 0, read)
+            }
+        }
+        digest.digest().encodeHex().toString()
+    }
+
+    /**
+     * Waits for the application to start serving, and stops waiting if the 
run ends first --
+     * otherwise a run that fails immediately is waited on for the whole 
timeout.
+     *
+     * <p>Either the run says it started or its port answers. The message is 
Spring Boot's and is
+     * the earlier and clearer of the two, but it is an INFO log an 
application is free to turn off
+     * or reword; a port that accepts a connection is the application's own 
doing and cannot be
+     * configured away while it is still an application worth training.</p>
+     */
+    private void awaitStarted(Process process, File output) {
+        long deadline = System.currentTimeMillis() + 
Duration.ofSeconds(startTimeoutSeconds.get()).toMillis()
+        while (System.currentTimeMillis() < deadline) {
+            if (!process.isAlive()) {
+                throw new GradleException('The training run ended before it 
started serving.' +
+                        whyItEnded(output) + " What it printed is in 
${output}")
+            }
+            if (output.isFile() && output.text.contains('Started ')) {

Review Comment:
   `03638e7424`. Added `StartupLog`, used by both tasks, matching `Started .+ 
in [\d.]+ seconds`.
   
   It also reads only what has been added since the last poll, decodes UTF-8 
explicitly, and matches whole lines, so a line still being written is left 
rather than cut in half. Added `StartupLogSpec` covering `Started 
ServerConnector@...`, `Started Server@...`, `Tomcat started on port 8080`, and 
a Spring Boot line arriving in two halves.
   



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/aot/TraceNativeMetadataTask.groovy:
##########
@@ -0,0 +1,484 @@
+/*
+ *  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.aot
+
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+import groovy.transform.CompileStatic
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/**
+ * Runs the application under GraalVM's tracing agent and writes down the 
reflection it did.
+ *
+ * <p>{@code GenerateNativeMetadataTask} records an application's own 
artefacts by reading the build
+ * output, which needs no run and misses nothing of the application's. What it 
cannot know is the
+ * framework's own reflection along a request path -- a controller method 
reached through Groovy's
+ * dispatch, a conversion asked for while binding a form -- because none of 
that is in the
+ * application's classes. An image built without it starts, serves its home 
page, and fails on the
+ * request that first takes such a path.</p>
+ *
+ * <p>The agent records exactly that, and records only what ran. So the paths 
are declared rather
+ * than discovered, and what is not listed is not covered:</p>
+ *
+ * <pre>
+ * grails {
+ *     nativeMetadata {
+ *         paths = ['/', '/login', '/book', '/book/create']
+ *         forms = ['/book/create']
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>A form is asked for, read, and submitted with the fields it declares, 
because posting fewer
+ * than it declares records less than the application does. A checkbox is a 
string on the wire and a
+ * boolean on the domain class, so a form submitted without one never asks the 
conversion service
+ * anything -- and an image built from that trace fails the first time someone 
ticks a box.</p>
+ *
+ * <p>Forms are submitted before paths are asked for, and the session one 
establishes is carried
+ * through the rest of the trace. A page behind a login is reached by listing 
the login form, which
+ * makes the pages named after it reachable:</p>
+ *
+ * <pre>
+ * grails {
+ *     nativeMetadata {
+ *         forms = ['/login?username=admin&amp;password=secret', 
'/book/create']
+ *         paths = ['/', '/book']
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>Where a page carries more than one form -- a layout's search or sign-out 
beside the page's own
+ * -- the one declaring the most fields is submitted, and which it was is 
reported. A page whose
+ * wanted form is not the fullest names it: {@code 
'/book/create#bookForm'}.</p>
+ *
+ * <p>Written to the application's sources rather than to the build directory: 
what an image was
+ * built from should be reviewable, and a trace is only as good as the paths 
someone thought of.</p>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+@DisableCachingByDefault(because = 'Records what a run of the application did, 
which is not an output of its inputs')
+abstract class TraceNativeMetadataTask extends DefaultTask {
+
+    /** How the agent is asked for, and the only way its output can be merged 
with what is there. */
+    private static final String AGENT = 'native-image-agent'
+
+    /** The names the agent library goes by, one of which is beside a 
GraalVM's java. */
+    private static final List<String> AGENT_LIBRARIES = [
+            'libnative-image-agent.dylib', 'libnative-image-agent.so', 
'native-image-agent.dll'
+    ]
+
+    private static final Pattern FORM = 
Pattern.compile(/(?is)<form\b[^>]*>.*?<\/form>/)
+    private static final Pattern ACTION = 
Pattern.compile(/(?i)\baction\s*=\s*"([^"]*)"/)
+    private static final Pattern ID = 
Pattern.compile(/(?i)\bid\s*=\s*"([^"]+)"/)
+    private static final Pattern FIELD = 
Pattern.compile(/(?is)<(?:input|select|textarea)\b[^>]*>/)
+    private static final Pattern NAME = 
Pattern.compile(/(?i)\bname\s*=\s*"([^"]+)"/)
+    private static final Pattern VALUE = 
Pattern.compile(/(?i)\bvalue\s*=\s*"([^"]*)"/)
+    private static final Pattern TYPE = 
Pattern.compile(/(?i)\btype\s*=\s*"([^"]+)"/)
+
+    /**
+     * Carries the session from one request to the next, which is what makes a 
form that
+     * authenticates worth submitting: the pages that follow it are only 
reachable once it has been.
+     *
+     * <p>Its own cookie store rather than the JVM's. The store belongs to the 
client, so nothing is
+     * left behind in a daemon that outlives the build and two traces at once 
cannot share one.</p>
+     */
+    private HttpClient client
+
+    /** The archive to run, which has to be the one the image will be built 
from. */
+    @InputFile
+    @PathSensitive(PathSensitivity.RELATIVE)
+    abstract RegularFileProperty getArchiveFile()
+
+    /**
+     * A java from a GraalVM. The agent ships with GraalVM rather than with a 
JDK, and an application
+     * built for an image is compiled for GraalVM's Java -- so the JDK that 
runs the build is usually
+     * neither, and running the trace on it fails at load or refuses the class 
file.
+     */
+    @Input
+    abstract Property<String> getJavaExecutable()
+
+    @Input
+    abstract ListProperty<String> getJvmArguments()
+
+    /** Paths to ask for. */
+    @Input
+    abstract ListProperty<String> getPaths()
+
+    /** Pages whose form is to be filled in and submitted. */
+    @Input
+    abstract ListProperty<String> getForms()
+
+    @Input
+    abstract Property<Integer> getPort()
+
+    @Input
+    abstract Property<Integer> getStartTimeoutSeconds()
+
+    /**
+     * Where the agent merges what it recorded. Not declared as an output: it 
is in the application's
+     * sources, and a directory Gradle believes it owns is a directory Gradle 
will delete.
+     */
+    @Internal
+    abstract DirectoryProperty getOutputDirectory()
+
+    @TaskAction
+    void trace() {
+        File java = new File(javaExecutable.get())
+        File metadata = outputDirectory.get().asFile
+        metadata.mkdirs()
+        refuseWithoutAgent(java)
+
+        List<String> command = []
+        command << java.absolutePath
+        command << 
"-agentlib:${AGENT}=config-merge-dir=${metadata.absolutePath}".toString()
+        command.addAll(jvmArguments.get())
+        command << '-jar' << archiveFile.get().asFile.absolutePath
+        command << "--server.port=${port.get()}".toString()
+
+        File output = new File(temporaryDir, 'trace.log')
+        Process process = new ProcessBuilder(command)
+                .directory(archiveFile.get().asFile.parentFile)
+                .redirectErrorStream(true)
+                .redirectOutput(output)
+                .start()
+
+        client = HttpClient.newBuilder()

Review Comment:
   `cb76065a1e`. Closed in the `finally` alongside `stop(process)`, field 
nulled. Corrected the javadoc that claimed nothing was left behind in the 
daemon.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to