1 package io.micronaut.build;
2
3 import io.methvin.watcher.DirectoryChangeEvent;
4 import io.methvin.watcher.DirectoryWatcher;
5 import io.micronaut.build.services.CompilerService;
6 import io.micronaut.build.services.ExecutorService;
7 import org.apache.maven.execution.MavenSession;
8 import org.apache.maven.model.FileSet;
9 import org.apache.maven.plugin.AbstractMojo;
10 import org.apache.maven.plugin.BuildPluginManager;
11 import org.apache.maven.plugin.MojoExecutionException;
12 import org.apache.maven.plugins.annotations.LifecyclePhase;
13 import org.apache.maven.plugins.annotations.Mojo;
14 import org.apache.maven.plugins.annotations.Parameter;
15 import org.apache.maven.plugins.annotations.ResolutionScope;
16 import org.apache.maven.project.*;
17 import org.apache.maven.toolchain.Toolchain;
18 import org.apache.maven.toolchain.ToolchainManager;
19 import org.codehaus.plexus.util.DirectoryScanner;
20 import org.codehaus.plexus.util.Os;
21 import org.eclipse.aether.RepositorySystemSession;
22 import org.eclipse.aether.graph.Dependency;
23 import org.eclipse.aether.graph.DependencyFilter;
24 import org.eclipse.aether.util.artifact.JavaScopes;
25 import org.eclipse.aether.util.filter.DependencyFilterUtils;
26
27 import javax.inject.Inject;
28 import java.io.File;
29 import java.io.IOException;
30 import java.nio.file.Path;
31 import java.util.*;
32 import java.util.stream.Collectors;
33
34 import static java.nio.file.Files.isDirectory;
35 import static java.nio.file.Files.isReadable;
36 import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
37
38
39
40
41
42
43
44
45
46
47
48
49 @SuppressWarnings("unused")
50 @Mojo(name = "run", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PREPARE_PACKAGE)
51 public class RunMojo extends AbstractMojo {
52
53 private static final int LAST_COMPILATION_THRESHOLD = 500;
54 private static final String JAVA = "java";
55 private static final List<String> DEFAULT_EXCLUDES;
56
57 static {
58 DEFAULT_EXCLUDES = new ArrayList<>();
59 Collections.addAll(DEFAULT_EXCLUDES, DirectoryScanner.DEFAULTEXCLUDES);
60 Collections.addAll(DEFAULT_EXCLUDES, "**/.idea/**");
61 }
62
63 private final MavenSession mavenSession;
64 private final ProjectDependenciesResolver resolver;
65 private final ProjectBuilder projectBuilder;
66 private final ToolchainManager toolchainManager;
67 private final String javaExecutable;
68 private final CompilerService compilerService;
69 private final Path projectRootDirectory;
70
71
72
73
74 @Parameter(defaultValue = "${project.build.directory}")
75 private File targetDirectory;
76
77
78
79
80
81 @Parameter(defaultValue = "${exec.mainClass}", required = true)
82 private String mainClass;
83
84
85
86
87 @Parameter(property = "mn.debug", defaultValue = "false")
88 private boolean debug;
89
90
91
92
93 @Parameter(property = "mn.debug.suspend", defaultValue = "false")
94 private boolean debugSuspend;
95
96
97
98
99 @Parameter(property = "mn.debug.port", defaultValue = "5005")
100 private int debugPort;
101
102
103
104
105
106
107
108
109 @Parameter
110 private List<FileSet> watches;
111
112
113
114
115
116
117 @Parameter(property = "mn.jvmArgs")
118 private List<String> jvmArguments;
119
120
121
122
123 @Parameter(property = "mn.appArgs")
124 private List<String> appArguments;
125
126
127
128
129 @Parameter(property = "mn.watch", defaultValue = "true")
130 private boolean watchForChanges;
131
132 private MavenProject mavenProject;
133 private DirectoryWatcher directoryWatcher;
134 private Process process;
135 private List<Dependency> projectDependencies;
136 private String classpath;
137 private int classpathHash;
138 private long lastCompilation;
139 private Map<String, Path> sourceDirectories;
140
141 @SuppressWarnings("CdiInjectionPointsInspection")
142 @Inject
143 public RunMojo(MavenProject mavenProject, MavenSession mavenSession, BuildPluginManager pluginManager,
144 ProjectDependenciesResolver resolver, ProjectBuilder projectBuilder, ToolchainManager toolchainManager,
145 CompilerService compilerService, ExecutorService executorService) {
146 this.mavenProject = mavenProject;
147 this.mavenSession = mavenSession;
148 this.resolver = resolver;
149 this.projectBuilder = projectBuilder;
150 this.projectRootDirectory = mavenProject.getBasedir().toPath();
151 this.toolchainManager = toolchainManager;
152 this.compilerService = compilerService;
153 this.javaExecutable = findJavaExecutable();
154
155 resolveDependencies();
156 this.classpathHash = this.classpath.hashCode();
157 }
158
159 @Override
160 @SuppressWarnings("unchecked")
161 public void execute() throws MojoExecutionException {
162 this.sourceDirectories = compilerService.resolveSourceDirectories();
163 if (compilerService.needsCompilation()) {
164 compilerService.compileProject(true);
165 }
166
167 try {
168 runApplication();
169 Thread shutdownHook = new Thread(this::killProcess);
170 Runtime.getRuntime().addShutdownHook(shutdownHook);
171
172 if (watchForChanges) {
173 List<Path> pathsToWatch = new ArrayList<>(sourceDirectories.values());
174 pathsToWatch.add(projectRootDirectory);
175
176 if (watches != null && !watches.isEmpty()) {
177 for (FileSet fs : watches) {
178 File directory = new File(fs.getDirectory());
179 if (directory.exists()) {
180 pathsToWatch.add(directory.toPath());
181
182 if ((fs.getIncludes() == null || fs.getIncludes().isEmpty()) && (fs.getExcludes() == null || fs.getExcludes().isEmpty())) {
183 fs.addInclude("**/*");
184 }
185 } else {
186 if (getLog().isWarnEnabled()) {
187 getLog().warn("The specified directory to watch doesn't exist: " + directory.getPath());
188 }
189 }
190 }
191 }
192
193 this.directoryWatcher = DirectoryWatcher
194 .builder()
195 .paths(pathsToWatch)
196 .listener(this::handleEvent)
197 .build();
198
199 if (getLog().isDebugEnabled()) {
200 getLog().debug("Watching for changes...");
201 }
202 this.directoryWatcher.watch();
203 } else if (process != null && process.isAlive()) {
204 process.waitFor();
205 }
206 } catch (Exception e) {
207 if (getLog().isDebugEnabled()) {
208 getLog().debug("Exception while watching for changes", e);
209 }
210 throw new MojoExecutionException("Exception while watching for changes", e);
211 } finally {
212 killProcess();
213 cleanup();
214 }
215 }
216
217 private void handleEvent(DirectoryChangeEvent event) throws IOException {
218 Path path = event.path();
219 Path parent = path.getParent();
220
221 if (parent.equals(projectRootDirectory)) {
222 if (path.endsWith("pom.xml") && rebuildMavenProject() && resolveDependencies() && classpathHasChanged()) {
223 if (getLog().isInfoEnabled()) {
224 getLog().info("Detected POM dependencies change. Restarting application");
225 }
226 runApplication();
227 }
228 } else if (matches(path)) {
229 if (getLog().isInfoEnabled()) {
230 getLog().info("Detected change in " + projectRootDirectory.relativize(path).toString());
231 }
232 boolean compiledOk = compileProject();
233 if (compiledOk) {
234 runApplication();
235 }
236 }
237 }
238
239 private boolean matches(Path path) {
240
241 if (isDefaultExcluded(path) || isDirectory(path, NOFOLLOW_LINKS) || !isReadable(path) || hasBeenCompiledRecently()) {
242 return false;
243 }
244
245
246 boolean matches = this.sourceDirectories
247 .values()
248 .stream()
249 .anyMatch(path.getParent()::startsWith);
250
251 String relativePath = projectRootDirectory.relativize(path).toString();
252
253 if (getLog().isDebugEnabled()) {
254 String belongs = matches ? "belongs" : "does not belong";
255 getLog().debug("Path [" + relativePath + "] " + belongs + " to a source directory");
256 }
257
258 if (watches != null && !watches.isEmpty()) {
259
260 if (!matches) {
261 for (FileSet fileSet : watches) {
262 if (fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty()) {
263 File directory = new File(fileSet.getDirectory());
264 if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath()))
265 for (String includePattern : fileSet.getIncludes()) {
266 if (DirectoryScanner.match(includePattern, path.toString()) || new File(directory, includePattern).toPath().toAbsolutePath().equals(path)) {
267 matches = true;
268 if (getLog().isDebugEnabled()) {
269 getLog().debug("Path [" + relativePath + "] matched the include pattern [" + includePattern + "] of the directory [" + fileSet.getDirectory() + "]");
270 }
271 break;
272 }
273 }
274 }
275 if (matches) break;
276 }
277 }
278
279
280 if (matches) {
281 for (FileSet fileSet : watches) {
282 if (fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty()) {
283 File directory = new File(fileSet.getDirectory());
284 if (directory.exists() && path.getParent().startsWith(directory.getAbsolutePath())) {
285 for (String excludePattern : fileSet.getExcludes()) {
286 if (DirectoryScanner.match(excludePattern, path.toString()) || new File(directory, excludePattern).toPath().toAbsolutePath().equals(path)) {
287 matches = false;
288 if (getLog().isDebugEnabled()) {
289 getLog().debug("Path [" + relativePath + "] matched the exclude pattern [" + excludePattern + "] of the directory [" + fileSet.getDirectory() + "]");
290 }
291 break;
292 }
293 }
294 }
295 }
296 if (!matches) break;
297 }
298 }
299 }
300
301 return matches;
302 }
303
304 private boolean isDefaultExcluded(Path path) {
305 return path.startsWith(targetDirectory.getAbsolutePath()) ||
306 DEFAULT_EXCLUDES.stream()
307 .anyMatch(excludePattern -> DirectoryScanner.match(excludePattern, path.toString()));
308 }
309
310 private boolean hasBeenCompiledRecently() {
311 return (System.currentTimeMillis() - lastCompilation) < LAST_COMPILATION_THRESHOLD;
312 }
313
314 private void cleanup() {
315 if (getLog().isDebugEnabled()) {
316 getLog().debug("Cleaning up");
317 }
318 try {
319 directoryWatcher.close();
320 } catch (Exception e) {
321
322 }
323 }
324
325 private boolean rebuildMavenProject() {
326 boolean success = true;
327 try {
328 ProjectBuildingRequest projectBuildingRequest = mavenSession.getProjectBuildingRequest();
329 projectBuildingRequest.setResolveDependencies(true);
330 ProjectBuildingResult build = projectBuilder.build(mavenProject.getArtifact(), projectBuildingRequest);
331 MavenProject project = build.getProject();
332 mavenProject = project;
333 mavenSession.setCurrentProject(project);
334 } catch (ProjectBuildingException e) {
335 success = false;
336 if (getLog().isWarnEnabled()) {
337 getLog().warn("Error while trying to build the Maven project model", e);
338 }
339 }
340 return success;
341 }
342
343 private boolean resolveDependencies() {
344 boolean success = true;
345 try {
346 DependencyFilter filter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE, JavaScopes.RUNTIME);
347 RepositorySystemSession session = mavenSession.getRepositorySession();
348 DependencyResolutionRequest dependencyResolutionRequest = new DefaultDependencyResolutionRequest(mavenProject, session);
349 dependencyResolutionRequest.setResolutionFilter(filter);
350 DependencyResolutionResult result = resolver.resolve(dependencyResolutionRequest);
351 this.projectDependencies = result.getDependencies();
352 buildClasspath();
353 } catch (DependencyResolutionException e) {
354 success = false;
355 if (getLog().isWarnEnabled()) {
356 getLog().warn("Error while trying to resolve dependencies for the current project", e);
357 }
358 }
359 return success;
360 }
361
362 private void buildClasspath() {
363 Comparator<Dependency> byGroupId = Comparator.comparing(d -> d.getArtifact().getGroupId());
364 Comparator<Dependency> byArtifactId = Comparator.comparing(d -> d.getArtifact().getArtifactId());
365 classpath = this.projectDependencies.stream()
366 .sorted(byGroupId.thenComparing(byArtifactId))
367 .map(dependency -> dependency.getArtifact().getFile().getAbsolutePath())
368 .collect(Collectors.joining(File.pathSeparator));
369 }
370
371 private boolean classpathHasChanged() {
372 int oldClasspathHash = this.classpathHash;
373 this.classpathHash = this.classpath.hashCode();
374 return oldClasspathHash != classpathHash;
375
376 }
377
378 private void runApplication() throws IOException {
379 String classpathArgument = new File(targetDirectory, "classes" + File.pathSeparator).getAbsolutePath() + this.classpath;
380 List<String> args = new ArrayList<>();
381 args.add(javaExecutable);
382
383 if (debug) {
384 String suspend = debugSuspend ? "y" : "n";
385 args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=" + suspend + ",address=" + debugPort);
386 }
387
388 if (jvmArguments != null && jvmArguments.size() > 0) {
389 args.addAll(jvmArguments);
390 }
391
392 if (mavenSession.getUserProperties().size() > 0) {
393 mavenSession.getUserProperties().forEach((k, v) -> args.add("-D" + k + "=" + v));
394 }
395
396 args.add("-classpath");
397 args.add(classpathArgument);
398 args.add("-XX:TieredStopAtLevel=1");
399 args.add("-Dcom.sun.management.jmxremote");
400 args.add(mainClass);
401
402 if (appArguments != null && appArguments.size() > 0) {
403 args.addAll(appArguments);
404 }
405
406 if (getLog().isDebugEnabled()) {
407 getLog().debug("Running " + String.join(" ", args));
408 }
409
410 killProcess();
411 process = new ProcessBuilder(args)
412 .inheritIO()
413 .directory(targetDirectory)
414 .start();
415 }
416
417 private String findJavaExecutable() {
418 Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", mavenSession);
419 if (toolchain != null) {
420 return toolchain.findTool(JAVA);
421 } else {
422 File javaBinariesDir = new File(new File(System.getProperty("java.home")), "bin");
423 if (Os.isFamily(Os.FAMILY_UNIX)) {
424 return new File(javaBinariesDir, JAVA).getAbsolutePath();
425 } else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
426 return new File(javaBinariesDir, "java.exe").getAbsolutePath();
427 } else {
428 return JAVA;
429 }
430 }
431 }
432
433 private boolean compileProject() {
434 Optional<Long> lastCompilation = compilerService.compileProject(true);
435 lastCompilation.ifPresent(lc -> this.lastCompilation = lc);
436 return lastCompilation.isPresent();
437 }
438
439 private void killProcess() {
440 if (getLog().isDebugEnabled()) {
441 getLog().debug("Stopping the background process");
442 }
443 if (process != null && process.isAlive()) {
444 process.destroy();
445 try {
446 process.waitFor();
447 } catch (InterruptedException e) {
448 process.destroyForcibly();
449 }
450 }
451 }
452
453 }