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