001/*
002 * Copyright 2017-2022 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven;
017
018import io.methvin.watcher.DirectoryChangeEvent;
019import io.methvin.watcher.DirectoryWatcher;
020import io.micronaut.core.annotation.Experimental;
021import io.micronaut.maven.aot.AbstractAotAnalysisMojo;
022import io.micronaut.maven.core.MojoUtils;
023import io.micronaut.maven.services.CompilerService;
024import io.micronaut.maven.services.DependencyResolutionService;
025import io.micronaut.maven.services.ExecutorService;
026import io.micronaut.maven.jsonschema.ValidateDevConfigurationMojo;
027import io.micronaut.maven.testresources.AbstractTestResourcesMojo;
028import io.micronaut.maven.testresources.TestResourcesHelper;
029import io.micronaut.testresources.buildtools.ServerSettings;
030import io.micronaut.testresources.buildtools.ServerUtils;
031import org.apache.maven.execution.MavenSession;
032import org.apache.maven.model.Build;
033import org.apache.maven.model.FileSet;
034import org.apache.maven.plugin.BuildPluginManager;
035import org.apache.maven.plugin.MojoExecutionException;
036import org.apache.maven.plugins.annotations.Execute;
037import org.apache.maven.plugins.annotations.LifecyclePhase;
038import org.apache.maven.plugins.annotations.Mojo;
039import org.apache.maven.plugins.annotations.Parameter;
040import org.apache.maven.plugins.annotations.ResolutionScope;
041import org.apache.maven.project.MavenProject;
042import org.apache.maven.project.ProjectBuilder;
043import org.apache.maven.project.ProjectBuildingException;
044import org.apache.maven.project.ProjectBuildingRequest;
045import org.apache.maven.project.ProjectBuildingResult;
046import org.apache.maven.toolchain.ToolchainManager;
047import org.codehaus.plexus.util.AbstractScanner;
048import org.codehaus.plexus.util.cli.CommandLineUtils;
049import org.codehaus.plexus.util.xml.Xpp3Dom;
050import org.eclipse.aether.graph.Dependency;
051import org.eclipse.aether.util.artifact.JavaScopes;
052
053import javax.inject.Inject;
054import java.io.File;
055import java.nio.file.Files;
056import java.nio.file.InvalidPathException;
057import java.nio.file.Path;
058import java.util.ArrayList;
059import java.util.Arrays;
060import java.util.Collections;
061import java.util.List;
062import java.util.Optional;
063import java.util.concurrent.atomic.AtomicBoolean;
064import java.util.concurrent.locks.ReentrantLock;
065
066import static io.micronaut.maven.core.MojoUtils.findJavaExecutable;
067import static io.micronaut.maven.core.MojoUtils.hasMicronautMavenPlugin;
068import static io.micronaut.maven.core.MojoUtils.THIS_PLUGIN;
069import static java.nio.file.Files.isDirectory;
070import static java.nio.file.Files.isReadable;
071import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
072
073/**
074 * <p>Executes a Micronaut application in development mode.</p>
075 *
076 * <p>It watches for changes in the project tree. If there are changes in the {@code pom.xml} file, dependencies will be reloaded. If
077 * the changes are anywhere underneath {@code src/main}, it will recompile the project and restart the application.</p>
078 *
079 * <p>The plugin can handle changes in Java, Groovy and resource source directories by default.</p>
080 *
081 * @author Álvaro Sánchez-Mariscal
082 * @since 1.0.0
083 */
084@SuppressWarnings("unused")
085@Mojo(name = "run", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PREPARE_PACKAGE, aggregator = true)
086@Execute(phase = LifecyclePhase.PROCESS_CLASSES)
087public class RunMojo extends AbstractTestResourcesMojo {
088
089    public static final String MN_APP_ARGS = "mn.appArgs";
090    public static final String EXEC_MAIN_CLASS = "${exec.mainClass}";
091    public static final String RESOURCES_DIR = "src/main/resources";
092
093    private static final List<String> RELEVANT_SRC_DIRS = List.of("resources", "java", "groovy");
094    private static final int LAST_COMPILATION_THRESHOLD = 500;
095    private static final List<String> DEFAULT_EXCLUDES;
096
097    static {
098        DEFAULT_EXCLUDES = new ArrayList<>();
099        Collections.addAll(DEFAULT_EXCLUDES, AbstractScanner.DEFAULTEXCLUDES);
100        Collections.addAll(DEFAULT_EXCLUDES, "**/.idea/**", "**/src/test/**");
101    }
102
103    private final MavenSession mavenSession;
104    private final ProjectBuilder projectBuilder;
105    private final ToolchainManager toolchainManager;
106    private final String javaExecutable;
107    private final DependencyResolutionService dependencyResolutionService;
108    private final CompilerService compilerService;
109    private final ExecutorService executorService;
110
111    /**
112     * The project's target directory.
113     */
114    private File targetDirectory;
115
116    /**
117     * The main class of the application, as defined in the
118     * <a href="https://www.mojohaus.org/exec-maven-plugin/java-mojo.html#mainClass">Exec Maven Plugin</a>.
119     */
120    @Parameter(defaultValue = EXEC_MAIN_CLASS)
121    private String mainClass;
122
123    /**
124     * Whether to start the Micronaut application in debug mode.
125     */
126    @Parameter(property = "mn.debug", defaultValue = "false")
127    private boolean debug;
128
129    /**
130     * Whether to suspend the execution of the application when running in debug mode.
131     */
132    @Parameter(property = "mn.debug.suspend", defaultValue = "false")
133    private boolean debugSuspend;
134
135    /**
136     * The port where remote debuggers can be attached to.
137     */
138    @Parameter(property = "mn.debug.port", defaultValue = "5005")
139    private int debugPort;
140
141    /**
142     * The host where remote debuggers can connect.
143     */
144    @Parameter(property = "mn.debug.host", defaultValue = "127.0.0.1")
145    private String debugHost;
146
147    /**
148     * List of inclusion/exclusion paths that should not trigger an application restart.
149     * For example, you can exclude a particular directory from being watched by adding the following
150     * configuration:
151     * <pre>
152     *     &lt;watches&gt;
153     *         &lt;watch&gt;
154     *             &lt;directory&gt;.some-dir&lt;/directory&gt;
155     *             &lt;excludes&gt;
156     *                 &lt;exclude&gt;**&#47;*&lt;/exclude&gt;
157     *             &lt;/excludes&gt;
158     *         &lt;/watch&gt;
159     *     &lt;/watches&gt;
160     * </pre>
161     * Check the
162     * <a href="https://maven.apache.org/ref/3.3.9/maven-model/apidocs/org/apache/maven/model/FileSet.html">FileSet</a>
163     * documentation for more details.
164     *
165     * @see <a href="https://maven.apache.org/ref/3.3.9/maven-model/apidocs/org/apache/maven/model/FileSet.html">FileSet</a>
166     */
167    @Parameter
168    private List<FileSet> watches;
169
170    /**
171     * <p>List of additional arguments that will be passed to the JVM process, such as Java agent properties.</p>
172     *
173     * <p>When using the command line, user properties will be passed through, eg: <code>mnv mn:run -Dmicronaut.environments=dev</code>.</p>
174     */
175    @Parameter(property = "mn.jvmArgs")
176    private String jvmArguments;
177
178    /**
179     * List of additional arguments that will be passed to the application, after the class name.
180     */
181    @Parameter(property = MN_APP_ARGS)
182    private String appArguments;
183
184    /**
185     * Whether to watch for changes, or finish the execution after the first run.
186     */
187    @Parameter(property = "mn.watch", defaultValue = "true")
188    private boolean watchForChanges;
189
190    /**
191     * Whether to enable or disable Micronaut AOT.
192     */
193    @Parameter(property = "micronaut.aot.enabled", defaultValue = "false")
194    private boolean aotEnabled;
195
196    /**
197     * <p><b>Experimental.</b> Whether to keep the classes of the dependency JARs in a static CDS (class data sharing)
198     * archive, so that the launches and restarts after the first one load them from the archive instead of parsing
199     * and verifying them again. It needs JDK 25 or later.</p>
200     *
201     * <p>The first launch records the classes it loads. When it ends, the archive is created in the background under
202     * {@code target/mn-cds}, and later launches and restarts use it until the dependencies, the JDK or the JVM options
203     * change. While this option is on, the class path starts with the dependency JARs, followed by the
204     * {@code target/classes} directories of this project and of the reactor projects it depends on, which stay outside
205     * the archive.</p>
206     *
207     * @since 5.1.0
208     */
209    @Experimental
210    @Parameter(property = "mn.classDataSharing", defaultValue = "false")
211    private boolean classDataSharing;
212
213    // These 2 flags are used in the context of watching for changes
214    // the first one makes sure that only one recompilation is processed at a time
215    private final AtomicBoolean recompileRequested = new AtomicBoolean();
216    // the second one makes sure that we wait for the server to be started before we restart it
217    // otherwise a process may be kept alive
218    private final ReentrantLock restartLock = new ReentrantLock();
219
220    private MavenProject runnableProject;
221    private DirectoryWatcher directoryWatcher;
222    private volatile Process process;
223    private String classpath;
224    private int classpathHash;
225    private long lastCompilation;
226    private TestResourcesHelper testResourcesHelper;
227    private ClassDataSharingSupport classDataSharingSupport;
228
229    @SuppressWarnings("CdiInjectionPointsInspection")
230    @Inject
231    public RunMojo(MavenSession mavenSession,
232                   BuildPluginManager pluginManager,
233                   ProjectBuilder projectBuilder,
234                   ToolchainManager toolchainManager,
235                   CompilerService compilerService,
236                   ExecutorService executorService,
237                   DependencyResolutionService dependencyResolutionService) {
238        this.mavenSession = mavenSession;
239        this.projectBuilder = projectBuilder;
240        this.toolchainManager = toolchainManager;
241        this.compilerService = compilerService;
242        this.executorService = executorService;
243        this.javaExecutable = findJavaExecutable(toolchainManager, mavenSession);
244        this.dependencyResolutionService = dependencyResolutionService;
245    }
246
247    @Override
248    public void execute() throws MojoExecutionException {
249        try {
250            initialize();
251        } catch (Exception e) {
252            throw new MojoExecutionException(e.getMessage());
253        }
254
255        try {
256            maybeStartTestResourcesServer();
257            runApplication();
258            Thread shutdownHook = new Thread(this::stopOnShutdown);
259            Runtime.getRuntime().addShutdownHook(shutdownHook);
260
261            if (process != null && process.isAlive()) {
262                if (watchForChanges) {
263                    var pathsToWatch = new ArrayList<Path>();
264                    for (FileSet fs : watches) {
265                        var directory = runnableProject.getBasedir().toPath().resolve(fs.getDirectory()).toAbsolutePath();
266                        if (Files.exists(directory)) {
267                            pathsToWatch.add(directory);
268                            //If neither includes nor excludes, add a default include
269                            if ((fs.getIncludes() == null || fs.getIncludes().isEmpty()) && (fs.getExcludes() == null || fs.getExcludes().isEmpty())) {
270                                fs.addInclude("**/*");
271                            }
272                        }
273                    }
274
275                    this.directoryWatcher = DirectoryWatcher
276                        .builder()
277                        .paths(pathsToWatch)
278                        .listener(this::handleEvent)
279                        .build();
280
281                    // We use the working directory as the root path, because
282                    // the top-level project information may not be what we
283                    // expect, in particular if we run from a submodule, or
284                    // that we run from root but with the "-pl" option.
285                    // We can safely do this because it's only used to display
286                    // information about paths being watched to the user, the
287                    // actual paths are unchanged.
288                    Path root = Path.of(".").toAbsolutePath();
289                    List<Path> pathList = pathsToWatch.stream()
290                        .map(root::relativize)
291                        .filter(s -> !s.toString().isEmpty())
292                        .filter(Files::exists)
293                        .sorted()
294                        .toList();
295                    getLog().info("👀 Watching for changes in " + pathList);
296                    this.directoryWatcher.watch();
297                } else if (process != null && process.isAlive()) {
298                    process.waitFor();
299                }
300            }
301        } catch (InterruptedException e) {
302            Thread.currentThread().interrupt();
303        } catch (Exception e) {
304            if (getLog().isDebugEnabled()) {
305                getLog().debug("Exception while watching for changes", e);
306            }
307            throw new MojoExecutionException("Exception while watching for changes", e);
308        } finally {
309            killProcess();
310            if (classDataSharingSupport != null) {
311                classDataSharingSupport.awaitBackgroundWork();
312            }
313            cleanup();
314        }
315    }
316
317    protected final void initialize() {
318        final MavenProject currentProject = mavenSession.getCurrentProject();
319        if (hasMicronautMavenPlugin(currentProject)) {
320            runnableProject = currentProject;
321        } else {
322            final List<MavenProject> projectsWithPlugin = mavenSession.getProjects().stream()
323                .filter(MojoUtils::hasMicronautMavenPlugin)
324                .toList();
325            if (projectsWithPlugin.size() == 1) {
326                runnableProject = projectsWithPlugin.get(0);
327                log.info("Running project %s".formatted(runnableProject.getArtifactId()));
328            } else {
329                throw new IllegalStateException("The Micronaut Maven Plugin is declared in the following projects: %s. Please specify the project to run with the -pl option."
330                    .formatted(projectsWithPlugin.stream().map(MavenProject::getArtifactId).toList()));
331            }
332        }
333        this.targetDirectory = new File(runnableProject.getBuild().getDirectory());
334        if (classDataSharing) {
335            this.classDataSharingSupport = new ClassDataSharingSupport(getLog(),
336                targetDirectory.toPath().resolve(ClassDataSharingSupport.DIRECTORY_NAME), javaExecutable, System.getenv());
337        }
338        this.testResourcesHelper = new TestResourcesHelper(testResourcesEnabled, shared, buildDirectory, explicitPort,
339                clientTimeout, serverIdleTimeoutMinutes, runnableProject, mavenSession, dependencyResolutionService,
340                toolchainManager, testResourcesVersion, classpathInference, testResourcesDependencies,
341                sharedServerNamespace, debugServer, false, testResourcesSystemProperties);
342        resolveDependencies();
343        if (watches == null) {
344            watches = new ArrayList<>();
345        }
346        // watch pom.xml file changes
347        mavenSession.getAllProjects().stream()
348            .filter(this::isDependencyOfRunnableProject)
349            .map(MavenProject::getBasedir)
350            .map(File::toPath)
351            .forEach(path -> {
352                var fileSet = new FileSet();
353                fileSet.setDirectory(path.toString());
354                fileSet.addInclude("pom.xml");
355                watches.add(fileSet);
356            });
357        // Add the default watch paths
358        mavenSession.getAllProjects().stream()
359            .filter(this::isDependencyOfRunnableProject)
360            .flatMap(p -> {
361                var basedir = p.getBasedir().toPath();
362                return RELEVANT_SRC_DIRS.stream().map(dir -> basedir.resolve("src/main/" + dir));
363            })
364            .forEach(path -> {
365                var fileSet = new FileSet();
366                fileSet.setDirectory(path.toString());
367                fileSet.addInclude("**/*");
368                watches.add(fileSet);
369            });
370
371        compileProject();
372    }
373
374    private boolean isDependencyOfRunnableProject(MavenProject mavenProject) {
375        return mavenProject.equals(runnableProject) || runnableProject.getDependencies().stream()
376            .anyMatch(d -> d.getGroupId().equals(mavenProject.getGroupId()) && d.getArtifactId().equals(mavenProject.getArtifactId()));
377    }
378
379    protected final void setWatches(List<FileSet> watches) {
380        this.watches = watches;
381    }
382
383    final void handleEvent(DirectoryChangeEvent event) {
384        Path path = event.path();
385        Path parent = path.getParent();
386        Path projectRootDirectory = mavenSession.getTopLevelProject().getBasedir().toPath();
387
388        if (matches(path)) {
389            if (getLog().isInfoEnabled()) {
390                getLog().info(String.format("📝 Detected change in %s. Recompiling/restarting...", projectRootDirectory.relativize(path)));
391            }
392            boolean compiledOk = compileProject();
393            if (compiledOk) {
394                try {
395                    runApplication();
396                } catch (Exception e) {
397                    getLog().error("Unable to run application: " + e.getMessage(), e);
398                }
399            }
400        }
401    }
402
403    private boolean matches(Path path) {
404        // Apply default exclusions
405        if (isDefaultExcluded(path) || isDirectory(path, NOFOLLOW_LINKS) || !isReadable(path) || hasBeenCompiledRecently()) {
406            return false;
407        }
408
409        Path projectRootDirectory = mavenSession.getTopLevelProject().getBasedir().toPath();
410
411        String relativePath = projectRootDirectory.relativize(path).toString();
412
413        boolean matches = false;
414        for (FileSet fileSet : watches) {
415            if (fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty()) {
416                var directory = new File(fileSet.getDirectory());
417                if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
418                    for (String includePattern : fileSet.getIncludes()) {
419                        if (pathMatches(includePattern, path) || patternEquals(path, includePattern, directory)) {
420                            matches = true;
421                            if (getLog().isDebugEnabled()) {
422                                getLog().debug("Path [" + relativePath + "] matched the include pattern [" + includePattern + "] of the directory [" + fileSet.getDirectory() + "]");
423                            }
424                            break;
425                        }
426                    }
427                }
428            }
429            if (matches) {
430                break;
431            }
432        }
433
434        // Finally, process excludes only if the path is matching
435        if (matches) {
436            for (FileSet fileSet : watches) {
437                if (fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty()) {
438                    File directory = new File(fileSet.getDirectory());
439                    if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
440                        for (String excludePattern : fileSet.getExcludes()) {
441                            if (pathMatches(excludePattern, path) || patternEquals(path, excludePattern, directory)) {
442                                matches = false;
443                                if (getLog().isDebugEnabled()) {
444                                    getLog().debug("Path [" + relativePath + "] matched the exclude pattern [" + excludePattern + "] of the directory [" + fileSet.getDirectory() + "]");
445                                }
446                                break;
447                            }
448                        }
449                    }
450                }
451                if (!matches) {
452                    break;
453                }
454            }
455        }
456
457        return matches;
458    }
459
460    private boolean isDefaultExcluded(Path path) {
461        boolean excludeTargetDirectory = true;
462        if (this.watches != null && !this.watches.isEmpty()) {
463            for (FileSet fileSet : this.watches) {
464                if (fileSet.getDirectory().equals(this.targetDirectory.getName())) {
465                    excludeTargetDirectory = false;
466                }
467            }
468        }
469        return (excludeTargetDirectory && path.startsWith(targetDirectory.getAbsolutePath())) ||
470            DEFAULT_EXCLUDES.stream()
471                .anyMatch(excludePattern -> pathMatches(excludePattern, path));
472    }
473
474    private boolean hasBeenCompiledRecently() {
475        return (System.currentTimeMillis() - lastCompilation) < LAST_COMPILATION_THRESHOLD;
476    }
477
478    private void cleanup() {
479        if (getLog().isDebugEnabled()) {
480            getLog().debug("Cleaning up");
481        }
482        try {
483            directoryWatcher.close();
484            maybeStopTestResourcesServer();
485        } catch (Exception e) {
486            // Do nothing
487        }
488    }
489
490    private boolean rebuildMavenProject() {
491        boolean success = true;
492        try {
493            ProjectBuildingRequest projectBuildingRequest = mavenSession.getProjectBuildingRequest();
494            projectBuildingRequest.setResolveDependencies(true);
495            ProjectBuildingResult build = projectBuilder.build(runnableProject.getArtifact(), projectBuildingRequest);
496            MavenProject project = build.getProject();
497            runnableProject = project;
498            mavenSession.setCurrentProject(project);
499        } catch (ProjectBuildingException e) {
500            success = false;
501            if (getLog().isWarnEnabled()) {
502                getLog().warn("Error while trying to build the Maven project model", e);
503            }
504        }
505        return success;
506    }
507
508    private boolean resolveDependencies() {
509        try {
510            List<Dependency> dependencies = compilerService.resolveDependencies(runnableProject, true, JavaScopes.PROVIDED, JavaScopes.COMPILE, JavaScopes.RUNTIME);
511            if (dependencies.isEmpty()) {
512                return false;
513            } else {
514                this.classpath = compilerService.buildClasspath(dependencies);
515                return true;
516            }
517        } finally {
518            if (classpath != null) {
519                this.classpathHash = this.classpath.hashCode();
520            }
521        }
522    }
523
524    private boolean classpathHasChanged() {
525        int oldClasspathHash = this.classpathHash;
526        this.classpathHash = this.classpath.hashCode();
527        return oldClasspathHash != classpathHash;
528
529    }
530
531    /**
532     * Runs or restarts the application. Only visible for testing, shouldn't
533     * be called directly.
534     *
535     * @throws Exception if something goes wrong while starting the application
536     */
537    protected void runApplication() throws Exception {
538        if (restartLock.getQueueLength() >= 1) {
539            // if there's more than one restart request, we'll handle them all at once
540            return;
541        }
542        restartLock.lock();
543        try {
544            // Validate Micronaut configuration for the dev environment before starting.
545            executorService.executeGoal(runnableProject, THIS_PLUGIN, ValidateDevConfigurationMojo.MOJO_NAME, configurationValidationConfiguration());
546            runAotIfNeeded();
547            List<String> args = buildRunArguments();
548
549            if (getLog().isDebugEnabled()) {
550                getLog().debug("Running " + String.join(" ", args));
551            }
552
553            killProcess();
554            process = new ProcessBuilder(args)
555                .inheritIO()
556                .directory(targetDirectory)
557                .start();
558            if (classDataSharingSupport != null) {
559                classDataSharingSupport.launched(process);
560            }
561        } finally {
562            restartLock.unlock();
563        }
564    }
565
566    private List<String> buildRunArguments() throws Exception {
567        final List<String> reactorOutputs = mavenSession.getAllProjects().stream()
568            .filter(this::isDependencyOfRunnableProject)
569            .map(MavenProject::getBuild)
570            .map(Build::getOutputDirectory)
571            .toList();
572        final String reactorClasses = String.join(File.pathSeparator, reactorOutputs);
573        String classpathArgument = String.join(File.pathSeparator, reactorClasses, this.classpath);
574        List<String> translatedJvmArguments = translateArguments(jvmArguments);
575
576        var args = new ArrayList<String>();
577        args.add(javaExecutable);
578        addDebugArguments(args);
579        addTestResourcesArguments(args);
580        args.addAll(translatedJvmArguments);
581        addUserProperties(args);
582        addNativeImageAgentArguments(args, translatedJvmArguments);
583        args.add("-classpath");
584        int classpathIndex = args.size();
585        args.add(classpathArgument);
586        args.add("-XX:TieredStopAtLevel=1");
587        int mainClassIndex = args.size();
588        args.add(resolveMainClass());
589        args.addAll(translateArguments(appArguments));
590        if (classDataSharingSupport != null) {
591            return classDataSharingSupport.prepareLaunch(args, classpathIndex, mainClassIndex, reactorOutputs, this.classpath);
592        }
593        return args;
594    }
595
596    private void addDebugArguments(List<String> args) {
597        if (debug) {
598            String suspend = debugSuspend ? "y" : "n";
599            args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + ",address=" + debugHost + ":" + debugPort);
600        }
601    }
602
603    private void addTestResourcesArguments(List<String> args) {
604        if (testResourcesEnabled) {
605            Path testResourcesSettingsDirectory = shared ? ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace) :
606                AbstractTestResourcesMojo.serverSettingsDirectoryOf(targetDirectory.toPath());
607            Optional<ServerSettings> serverSettings = ServerUtils.readServerSettings(testResourcesSettingsDirectory);
608            serverSettings.ifPresent(settings -> testResourcesHelper.computeSystemProperties(settings)
609                .forEach((k, v) -> args.add("-D" + k + "=" + v)));
610        }
611    }
612
613    private void addNativeImageAgentArguments(List<String> args, List<String> translatedJvmArguments) throws MojoExecutionException {
614        List<String> nativeImageAgentArguments = NativeImageAgentSupport.computeJvmArguments(mavenSession, runnableProject, targetDirectory, translatedJvmArguments);
615        if (!nativeImageAgentArguments.isEmpty()) {
616            if (watchForChanges) {
617                getLog().warn("Native image agent metadata collection is intended for one-shot runs. Prefer mn:run -Dagent=true -Dmn.watch=false");
618            }
619            args.addAll(nativeImageAgentArguments);
620        }
621    }
622
623    private void addUserProperties(List<String> args) {
624        if (!mavenSession.getUserProperties().isEmpty()) {
625            mavenSession.getUserProperties().forEach((k, v) -> args.add("-D" + k + "=" + v));
626        }
627    }
628
629    private String resolveMainClass() {
630        if (mainClass == null) {
631            mainClass = runnableProject.getProperties().getProperty("exec.mainClass");
632        }
633        return mainClass;
634    }
635
636    private List<String> translateArguments(String arguments) throws Exception {
637        if (arguments == null || arguments.isEmpty()) {
638            return List.of();
639        }
640        return Arrays.asList(CommandLineUtils.translateCommandline(arguments));
641    }
642
643    private void runAotIfNeeded() {
644        if (aotEnabled) {
645            try {
646                executorService.executeGoal(runnableProject, THIS_PLUGIN, AbstractAotAnalysisMojo.NAME, aotAnalysisConfiguration());
647            } catch (MojoExecutionException e) {
648                getLog().error(e.getMessage());
649            }
650        }
651    }
652
653    private Xpp3Dom aotAnalysisConfiguration() {
654        Xpp3Dom configuration = new Xpp3Dom("configuration");
655        Xpp3Dom enabled = new Xpp3Dom("enabled");
656        enabled.setValue(Boolean.TRUE.toString());
657        configuration.addChild(enabled);
658        return configuration;
659    }
660
661    private Xpp3Dom configurationValidationConfiguration() {
662        Xpp3Dom configuration = new Xpp3Dom("configuration");
663        var plugin = runnableProject.getPlugin(THIS_PLUGIN);
664        if (plugin == null) {
665            return configuration;
666        }
667        Object pluginConfiguration = plugin.getConfiguration();
668        if (!(pluginConfiguration instanceof Xpp3Dom pluginDom)) {
669            return configuration;
670        }
671        Xpp3Dom validationConfiguration = pluginDom.getChild("configurationValidation");
672        if (validationConfiguration != null) {
673            configuration.addChild(new Xpp3Dom(validationConfiguration));
674        }
675        return configuration;
676    }
677
678    private void maybeStartTestResourcesServer() throws MojoExecutionException {
679        testResourcesHelper.start();
680    }
681
682    private void maybeStopTestResourcesServer() throws MojoExecutionException {
683        testResourcesHelper.stop(true);
684    }
685
686    private boolean compileProject() {
687        // There can be multiple changes detected at the same time, so we want
688        // to keep only one compilation request
689        if (recompileRequested.get()) {
690            return false;
691        }
692        recompileRequested.set(true);
693        try {
694            return doCompile();
695        } finally {
696            recompileRequested.set(false);
697        }
698    }
699
700    private boolean doCompile() {
701        Optional<Long> lastCompilationMillis = compilerService.compileProject();
702        lastCompilationMillis.ifPresent(lc -> this.lastCompilation = lc);
703        return lastCompilationMillis.isPresent();
704    }
705
706    private void killProcess() {
707        Process current = process;
708        if (current == null) {
709            return;
710        }
711        boolean stopped = false;
712        if (current.isAlive()) {
713            if (getLog().isDebugEnabled()) {
714                getLog().debug("Stopping the background process");
715            }
716            current.destroy();
717            stopped = true;
718            try {
719                current.waitFor();
720            } catch (InterruptedException e) {
721                current.destroyForcibly();
722                Thread.currentThread().interrupt();
723            }
724        }
725        if (classDataSharingSupport != null) {
726            classDataSharingSupport.launchEnded(current, stopped);
727        }
728    }
729
730    private void stopOnShutdown() {
731        if (classDataSharingSupport != null) {
732            classDataSharingSupport.shutdown();
733        }
734        killProcess();
735    }
736
737    private static String normalize(Path path) {
738        return path.toString().replace('\\', '/');
739    }
740
741    private static boolean pathMatches(String pattern, Path path) {
742        return AbstractScanner.match(pattern, normalize(path));
743    }
744
745    private static boolean patternEquals(Path path, String includePattern, File directory) {
746        try {
747            var testPath = normalize(directory.toPath().resolve(includePattern).toAbsolutePath());
748            return testPath.equals(normalize(path.toAbsolutePath()));
749        } catch (InvalidPathException ex) {
750            return false;
751        }
752    }
753
754}