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