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 * <watches> 152 * <watch> 153 * <directory>.some-dir</directory> 154 * <excludes> 155 * <exclude>**/*</exclude> 156 * </excludes> 157 * </watch> 158 * </watches> 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}