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