1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.micronaut.build;
17
18 import io.methvin.watcher.DirectoryChangeEvent;
19 import io.methvin.watcher.DirectoryWatcher;
20 import io.micronaut.build.aot.AotAnalysisMojo;
21 import io.micronaut.build.services.CompilerService;
22 import io.micronaut.build.services.DependencyResolutionService;
23 import io.micronaut.build.services.ExecutorService;
24 import io.micronaut.build.testresources.AbstractTestResourcesMojo;
25 import io.micronaut.build.testresources.TestResourcesHelper;
26 import io.micronaut.testresources.buildtools.ServerUtils;
27 import org.apache.maven.execution.MavenSession;
28 import org.apache.maven.model.FileSet;
29 import org.apache.maven.plugin.BuildPluginManager;
30 import org.apache.maven.plugin.MojoExecutionException;
31 import org.apache.maven.plugins.annotations.*;
32 import org.apache.maven.project.*;
33 import org.apache.maven.toolchain.ToolchainManager;
34 import org.codehaus.plexus.util.AbstractScanner;
35 import org.codehaus.plexus.util.cli.CommandLineUtils;
36 import org.eclipse.aether.artifact.Artifact;
37 import org.eclipse.aether.graph.Dependency;
38 import org.eclipse.aether.resolution.ArtifactResult;
39 import org.eclipse.aether.resolution.DependencyResolutionException;
40 import org.eclipse.aether.util.artifact.JavaScopes;
41
42 import javax.inject.Inject;
43 import java.io.File;
44 import java.nio.file.Files;
45 import java.nio.file.Path;
46 import java.util.*;
47 import java.util.stream.Stream;
48
49 import static io.micronaut.build.MojoUtils.findJavaExecutable;
50 import static io.micronaut.build.services.DependencyResolutionService.testResourcesModuleToAetherArtifact;
51 import static java.nio.file.Files.isDirectory;
52 import static java.nio.file.Files.isReadable;
53 import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
54
55
56
57
58
59
60
61
62
63
64
65
66 @SuppressWarnings("unused")
67 @Mojo(name = "run", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PREPARE_PACKAGE)
68 @Execute(phase = LifecyclePhase.PROCESS_CLASSES)
69 public class RunMojo extends AbstractTestResourcesMojo {
70
71 public static final String MN_APP_ARGS = "mn.appArgs";
72 public static final String EXEC_MAIN_CLASS = "${exec.mainClass}";
73 public static final String RESOURCES_DIR = "src/main/resources";
74 public static final String THIS_PLUGIN = "io.micronaut.build:micronaut-maven-plugin";
75
76 private static final int LAST_COMPILATION_THRESHOLD = 500;
77 private static final List<String> DEFAULT_EXCLUDES;
78
79 static {
80 DEFAULT_EXCLUDES = new ArrayList<>();
81 Collections.addAll(DEFAULT_EXCLUDES, AbstractScanner.DEFAULTEXCLUDES);
82 Collections.addAll(DEFAULT_EXCLUDES, "**/.idea/**");
83 }
84
85 private final MavenSession mavenSession;
86 private final ProjectBuilder projectBuilder;
87 private final ToolchainManager toolchainManager;
88 private final String javaExecutable;
89 private final DependencyResolutionService dependencyResolutionService;
90 private final CompilerService compilerService;
91 private final ExecutorService executorService;
92 private final Path projectRootDirectory;
93
94
95
96
97 @Parameter(defaultValue = "${project.build.directory}")
98 private File targetDirectory;
99
100
101
102
103
104 @Parameter(defaultValue = EXEC_MAIN_CLASS, required = true)
105 private String mainClass;
106
107
108
109
110 @Parameter(property = "mn.debug", defaultValue = "false")
111 private boolean debug;
112
113
114
115
116 @Parameter(property = "mn.debug.suspend", defaultValue = "false")
117 private boolean debugSuspend;
118
119
120
121
122 @Parameter(property = "mn.debug.port", defaultValue = "5005")
123 private int debugPort;
124
125
126
127
128 @Parameter(property = "mn.debug.host", defaultValue = "127.0.0.1")
129 private String debugHost;
130
131
132
133
134
135
136
137
138 @Parameter
139 private List<FileSet> watches;
140
141
142
143
144
145
146 @Parameter(property = "mn.jvmArgs")
147 private String jvmArguments;
148
149
150
151
152 @Parameter(property = MN_APP_ARGS)
153 private String appArguments;
154
155
156
157
158 @Parameter(property = "mn.watch", defaultValue = "true")
159 private boolean watchForChanges;
160
161
162
163
164 @Parameter(property = "micronaut.aot.enabled", defaultValue = "false")
165 private boolean aotEnabled;
166
167 private MavenProject mavenProject;
168 private DirectoryWatcher directoryWatcher;
169 private Process process;
170 private String classpath;
171 private int classpathHash;
172 private long lastCompilation;
173 private Map<String, Path> sourceDirectories;
174 private TestResourcesHelper testResourcesHelper;
175
176 @SuppressWarnings("CdiInjectionPointsInspection")
177 @Inject
178 public RunMojo(MavenProject mavenProject,
179 MavenSession mavenSession,
180 BuildPluginManager pluginManager,
181 ProjectBuilder projectBuilder,
182 ToolchainManager toolchainManager,
183 CompilerService compilerService,
184 ExecutorService executorService,
185 DependencyResolutionService dependencyResolutionService) {
186 this.mavenProject = mavenProject;
187 this.mavenSession = mavenSession;
188 this.projectBuilder = projectBuilder;
189 this.projectRootDirectory = mavenProject.getBasedir().toPath();
190 this.toolchainManager = toolchainManager;
191 this.compilerService = compilerService;
192 this.executorService = executorService;
193 this.javaExecutable = findJavaExecutable(toolchainManager, mavenSession);
194 this.dependencyResolutionService = dependencyResolutionService;
195 }
196
197 @Override
198 public void execute() throws MojoExecutionException {
199 testResourcesHelper = new TestResourcesHelper(testResourcesEnabled, keepAlive, shared, buildDirectory,
200 explicitPort, clientTimeout, mavenProject, mavenSession,
201 dependencyResolutionService, toolchainManager, testResourcesVersion,
202 classpathInference, testResourcesDependencies, sharedServerNamespace);
203 resolveDependencies();
204 this.sourceDirectories = compilerService.resolveSourceDirectories();
205
206 try {
207 maybeStartTestResourcesServer();
208 runApplication();
209 Thread shutdownHook = new Thread(this::killProcess);
210 Runtime.getRuntime().addShutdownHook(shutdownHook);
211
212 if (process != null && process.isAlive()) {
213 if (watchForChanges) {
214 List<Path> pathsToWatch = new ArrayList<>(sourceDirectories.values());
215 pathsToWatch.add(projectRootDirectory);
216 pathsToWatch.add(projectRootDirectory.resolve(RESOURCES_DIR));
217
218 if (watches != null && !watches.isEmpty()) {
219 for (FileSet fs : watches) {
220 File directory = new File(fs.getDirectory());
221 if (directory.exists()) {
222 pathsToWatch.add(directory.toPath());
223
224 if ((fs.getIncludes() == null || fs.getIncludes().isEmpty()) && (fs.getExcludes() == null || fs.getExcludes().isEmpty())) {
225 fs.addInclude("**/*");
226 }
227 } else {
228 if (getLog().isWarnEnabled()) {
229 getLog().warn("The specified directory to watch doesn't exist: " + directory.getPath());
230 }
231 }
232 }
233 }
234
235 this.directoryWatcher = DirectoryWatcher
236 .builder()
237 .paths(pathsToWatch)
238 .listener(this::handleEvent)
239 .build();
240
241 if (getLog().isDebugEnabled()) {
242 getLog().debug("Watching for changes...");
243 }
244 this.directoryWatcher.watch();
245 } else if (process != null && process.isAlive()) {
246 process.waitFor();
247 }
248 }
249 } catch (InterruptedException e) {
250 Thread.currentThread().interrupt();
251 } catch (Exception e) {
252 if (getLog().isDebugEnabled()) {
253 getLog().debug("Exception while watching for changes", e);
254 }
255 throw new MojoExecutionException("Exception while watching for changes", e);
256 } finally {
257 killProcess();
258 cleanup();
259 }
260 }
261
262 private void handleEvent(DirectoryChangeEvent event) {
263 Path path = event.path();
264 Path parent = path.getParent();
265
266 if (parent.equals(projectRootDirectory)) {
267 if (path.endsWith("pom.xml") && rebuildMavenProject() && resolveDependencies() && classpathHasChanged()) {
268 if (getLog().isInfoEnabled()) {
269 getLog().info("Detected POM dependencies change. Restarting application");
270 }
271 try {
272 runApplication();
273 } catch (Exception e) {
274 getLog().error("Unable to run application: " + e.getMessage(), e);
275 }
276 }
277 } else if (matches(path)) {
278 if (getLog().isInfoEnabled()) {
279 getLog().info("Detected change in " + projectRootDirectory.relativize(path));
280 }
281 boolean compiledOk = compileProject();
282 if (compiledOk) {
283 try {
284 runApplication();
285 } catch (Exception e) {
286 getLog().error("Unable to run application: " + e.getMessage(), e);
287 }
288 }
289 }
290 }
291
292 private boolean matches(Path path) {
293
294 if (isDefaultExcluded(path) || isDirectory(path, NOFOLLOW_LINKS) || !isReadable(path) || hasBeenCompiledRecently()) {
295 return false;
296 }
297
298
299 Collection<Path> values = this.sourceDirectories.values();
300 Collection<Path> pathsToCheck = new ArrayList<>(values.size() + 1);
301 pathsToCheck.addAll(values);
302 pathsToCheck.add(projectRootDirectory.resolve(RESOURCES_DIR));
303 boolean matches = pathsToCheck.stream().anyMatch(path.getParent()::startsWith);
304
305 String relativePath = projectRootDirectory.relativize(path).toString();
306
307 if (getLog().isDebugEnabled()) {
308 String belongs = matches ? "belongs" : "does not belong";
309 getLog().debug("Path [" + relativePath + "] " + belongs + " to a source directory");
310 }
311
312 if (watches != null && !watches.isEmpty()) {
313
314 if (!matches) {
315 for (FileSet fileSet : watches) {
316 if (fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty()) {
317 File directory = new File(fileSet.getDirectory());
318 if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
319 for (String includePattern : fileSet.getIncludes()) {
320 if (AbstractScanner.match(includePattern, path.toString()) || new File(directory, includePattern).toPath().toAbsolutePath().equals(path)) {
321 matches = true;
322 if (getLog().isDebugEnabled()) {
323 getLog().debug("Path [" + relativePath + "] matched the include pattern [" + includePattern + "] of the directory [" + fileSet.getDirectory() + "]");
324 }
325 break;
326 }
327 }
328 }
329 }
330 if (matches) {
331 break;
332 }
333 }
334 }
335
336
337 if (matches) {
338 for (FileSet fileSet : watches) {
339 if (fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty()) {
340 File directory = new File(fileSet.getDirectory());
341 if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
342 for (String excludePattern : fileSet.getExcludes()) {
343 if (AbstractScanner.match(excludePattern, path.toString()) || new File(directory, excludePattern).toPath().toAbsolutePath().equals(path)) {
344 matches = false;
345 if (getLog().isDebugEnabled()) {
346 getLog().debug("Path [" + relativePath + "] matched the exclude pattern [" + excludePattern + "] of the directory [" + fileSet.getDirectory() + "]");
347 }
348 break;
349 }
350 }
351 }
352 }
353 if (!matches) {
354 break;
355 }
356 }
357 }
358 }
359
360 return matches;
361 }
362
363 private boolean isDefaultExcluded(Path path) {
364 boolean excludeTargetDirectory = true;
365 if (this.watches != null && !this.watches.isEmpty()) {
366 for (FileSet fileSet : this.watches) {
367 if (fileSet.getDirectory().equals(this.targetDirectory.getName())) {
368 excludeTargetDirectory = false;
369 }
370 }
371 }
372 return (excludeTargetDirectory && path.startsWith(targetDirectory.getAbsolutePath())) ||
373 DEFAULT_EXCLUDES.stream()
374 .anyMatch(excludePattern -> AbstractScanner.match(excludePattern, path.toString()));
375 }
376
377 private boolean hasBeenCompiledRecently() {
378 return (System.currentTimeMillis() - lastCompilation) < LAST_COMPILATION_THRESHOLD;
379 }
380
381 private void cleanup() {
382 if (getLog().isDebugEnabled()) {
383 getLog().debug("Cleaning up");
384 }
385 try {
386 directoryWatcher.close();
387 maybeStopTestResourcesServer();
388 } catch (Exception e) {
389
390 }
391 }
392
393 private boolean rebuildMavenProject() {
394 boolean success = true;
395 try {
396 ProjectBuildingRequest projectBuildingRequest = mavenSession.getProjectBuildingRequest();
397 projectBuildingRequest.setResolveDependencies(true);
398 ProjectBuildingResult build = projectBuilder.build(mavenProject.getArtifact(), projectBuildingRequest);
399 MavenProject project = build.getProject();
400 mavenProject = project;
401 mavenSession.setCurrentProject(project);
402 } catch (ProjectBuildingException e) {
403 success = false;
404 if (getLog().isWarnEnabled()) {
405 getLog().warn("Error while trying to build the Maven project model", e);
406 }
407 }
408 return success;
409 }
410
411 private boolean resolveDependencies() {
412 try {
413 List<Dependency> dependencies = compilerService.resolveDependencies(JavaScopes.COMPILE, JavaScopes.RUNTIME);
414 if (testResourcesEnabled) {
415 Artifact clientArtifact = testResourcesModuleToAetherArtifact("client", testResourcesVersion);
416 Dependency dependency = new Dependency(clientArtifact, JavaScopes.RUNTIME);
417 try {
418 List<ArtifactResult> results = dependencyResolutionService.artifactResultsFor(Stream.of(clientArtifact), true);
419 results.forEach(r -> {
420 if (r.isResolved()) {
421 dependencies.add(new Dependency(r.getArtifact(), JavaScopes.RUNTIME));
422 }
423 });
424 } catch (DependencyResolutionException e) {
425 getLog().warn("Unable to resolve test resources client dependencies", e);
426 }
427 }
428 if (dependencies.isEmpty()) {
429 return false;
430 } else {
431 this.classpath = compilerService.buildClasspath(dependencies);
432 return true;
433 }
434 } finally {
435 if (classpath != null) {
436 this.classpathHash = this.classpath.hashCode();
437 }
438 }
439 }
440
441 private boolean classpathHasChanged() {
442 int oldClasspathHash = this.classpathHash;
443 this.classpathHash = this.classpath.hashCode();
444 return oldClasspathHash != classpathHash;
445
446 }
447
448 private void runApplication() throws Exception {
449 runAotIfNeeded();
450 String classpathArgument = new File(targetDirectory, "classes" + File.pathSeparator).getAbsolutePath() + this.classpath;
451 if (testResourcesEnabled) {
452 Path testResourcesSettingsDirectory = shared ? ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace) :
453 AbstractTestResourcesMojo.serverSettingsDirectoryOf(targetDirectory.toPath());
454 if (Files.isDirectory(testResourcesSettingsDirectory)) {
455 classpathArgument += File.pathSeparator + testResourcesSettingsDirectory.toAbsolutePath();
456 }
457 }
458 List<String> args = new ArrayList<>();
459 args.add(javaExecutable);
460
461 if (debug) {
462 String suspend = debugSuspend ? "y" : "n";
463 args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + ",address=" + debugHost + ":" + debugPort);
464 }
465
466 if (jvmArguments != null && !jvmArguments.isEmpty()) {
467 final String[] strings = CommandLineUtils.translateCommandline(jvmArguments);
468 args.addAll(Arrays.asList(strings));
469 }
470
471 if (!mavenSession.getUserProperties().isEmpty()) {
472 mavenSession.getUserProperties().forEach((k, v) -> args.add("-D" + k + "=" + v));
473 }
474
475 args.add("-classpath");
476 args.add(classpathArgument);
477 args.add("-XX:TieredStopAtLevel=1");
478 args.add("-Dcom.sun.management.jmxremote");
479 args.add(mainClass);
480
481 if (appArguments != null && !appArguments.isEmpty()) {
482 final String[] strings = CommandLineUtils.translateCommandline(appArguments);
483 args.addAll(Arrays.asList(strings));
484 }
485
486 if (getLog().isDebugEnabled()) {
487 getLog().debug("Running " + String.join(" ", args));
488 }
489
490 killProcess();
491 process = new ProcessBuilder(args)
492 .inheritIO()
493 .directory(targetDirectory)
494 .start();
495 }
496
497 private void runAotIfNeeded() {
498 if (aotEnabled) {
499 try {
500 executorService.executeGoal(THIS_PLUGIN, AotAnalysisMojo.NAME);
501 } catch (MojoExecutionException e) {
502 getLog().error(e);
503 }
504 }
505 }
506
507 private void maybeStartTestResourcesServer() throws MojoExecutionException {
508 testResourcesHelper.start();
509 }
510
511 private void maybeStopTestResourcesServer() throws MojoExecutionException {
512 testResourcesHelper.stop();
513 }
514
515 private boolean compileProject() {
516 Optional<Long> lastCompilationMillis = compilerService.compileProject(true);
517 lastCompilationMillis.ifPresent(lc -> this.lastCompilation = lc);
518 return lastCompilationMillis.isPresent();
519 }
520
521 private void killProcess() {
522 if (getLog().isDebugEnabled()) {
523 getLog().debug("Stopping the background process");
524 }
525 if (process != null && process.isAlive()) {
526 process.destroy();
527 try {
528 process.waitFor();
529 } catch (InterruptedException e) {
530 process.destroyForcibly();
531 Thread.currentThread().interrupt();
532 }
533 }
534 }
535
536 }