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.testresources; 017 018import io.micronaut.core.io.socket.SocketUtils; 019import io.micronaut.maven.services.DependencyResolutionService; 020import io.micronaut.testresources.buildtools.MavenDependency; 021import io.micronaut.testresources.buildtools.ModuleIdentifier; 022import io.micronaut.testresources.buildtools.ServerFactory; 023import io.micronaut.testresources.buildtools.ServerSettings; 024import io.micronaut.testresources.buildtools.ServerUtils; 025import io.micronaut.testresources.buildtools.TestResourcesClasspath; 026import org.apache.maven.execution.MavenSession; 027import org.apache.maven.model.Dependency; 028import org.apache.maven.plugin.MojoExecutionException; 029import org.apache.maven.plugin.logging.Log; 030import org.apache.maven.plugin.logging.SystemStreamLog; 031import org.apache.maven.project.MavenProject; 032import org.apache.maven.toolchain.ToolchainManager; 033import org.eclipse.aether.artifact.Artifact; 034import org.eclipse.aether.resolution.DependencyResolutionException; 035 036import java.io.File; 037import java.nio.file.DirectoryNotEmptyException; 038import java.nio.file.FileAlreadyExistsException; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.OutputStream; 042import java.nio.file.Files; 043import java.nio.file.LinkOption; 044import java.nio.file.Path; 045import java.nio.file.StandardCopyOption; 046import java.nio.file.StandardOpenOption; 047import java.nio.file.attribute.FileAttribute; 048import java.nio.file.attribute.PosixFilePermissions; 049import java.util.Collections; 050import java.util.HashMap; 051import java.util.HashSet; 052import java.util.LinkedHashMap; 053import java.util.List; 054import java.util.Map; 055import java.util.Optional; 056import java.util.Properties; 057import java.util.Set; 058import java.util.WeakHashMap; 059import java.util.UUID; 060import java.util.concurrent.ConcurrentHashMap; 061import java.util.concurrent.atomic.AtomicBoolean; 062import java.util.concurrent.locks.ReentrantLock; 063import java.util.stream.Stream; 064 065import static io.micronaut.maven.services.DependencyResolutionService.toClasspathFiles; 066import static io.micronaut.testresources.buildtools.ServerUtils.PROPERTIES_FILE_NAME; 067import static java.util.stream.Stream.concat; 068 069/** 070 * Utility class to stop Test Resources service. 071 */ 072public class TestResourcesHelper { 073 074 private static final String TEST_RESOURCES_PROPERTIES = PROPERTIES_FILE_NAME; 075 private static final String PORT_FILE_NAME = "test-resources-port.txt"; 076 private static final String APPLICATION_TEST_PROPERTIES = "application-test.properties"; 077 private static final String TEST_RESOURCES_SCOPE_PROPERTY = "micronaut.test.resources.scope"; 078 private static final String SCOPE_PREFIX = "mvn"; 079 080 private static final String TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX = "micronaut.test.resources."; 081 082 private static final String TEST_RESOURCES_PROP_SERVER_URI = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.uri"; 083 private static final String TEST_RESOURCES_PROP_ACCESS_TOKEN = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.access.token"; 084 private static final String TEST_RESOURCES_PROP_CLIENT_READ_TIMEOUT = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.client.read.timeout"; 085 private static final Object SESSION_STATE_MONITOR = new Object(); 086 private static final Map<MavenSession, SessionState> SESSION_STATES = new WeakHashMap<>(); 087 private static final Map<Path, ReentrantLock> SHARED_SERVER_LOCKS = new ConcurrentHashMap<>(); 088 089 private final boolean enabled; 090 091 private final MavenSession mavenSession; 092 093 private final boolean shared; 094 095 private final File buildDirectory; 096 097 private final Log log; 098 099 private Integer explicitPort; 100 101 private Integer clientTimeout; 102 103 private Integer serverIdleTimeoutMinutes; 104 105 private MavenProject mavenProject; 106 107 private DependencyResolutionService dependencyResolutionService; 108 109 private ToolchainManager toolchainManager; 110 111 private String testResourcesVersion; 112 113 private boolean classpathInference; 114 115 private List<Dependency> testResourcesDependencies; 116 117 private String sharedServerNamespace; 118 119 private boolean debugServer; 120 121 private boolean foreground = false; 122 123 private Map<String, String> testResourcesSystemProperties; 124 125 public TestResourcesHelper(boolean enabled, 126 boolean shared, 127 File buildDirectory, 128 Integer explicitPort, 129 Integer clientTimeout, 130 Integer serverIdleTimeoutMinutes, 131 MavenProject mavenProject, 132 MavenSession mavenSession, 133 DependencyResolutionService dependencyResolutionService, 134 ToolchainManager toolchainManager, 135 String testResourcesVersion, 136 boolean classpathInference, 137 List<Dependency> testResourcesDependencies, 138 String sharedServerNamespace, 139 boolean debugServer, 140 boolean foreground, final Map<String, String> testResourcesSystemProperties) { 141 this(mavenSession, enabled, shared, buildDirectory); 142 this.explicitPort = explicitPort; 143 this.clientTimeout = clientTimeout; 144 this.serverIdleTimeoutMinutes = serverIdleTimeoutMinutes; 145 this.mavenProject = mavenProject; 146 this.dependencyResolutionService = dependencyResolutionService; 147 this.toolchainManager = toolchainManager; 148 this.testResourcesVersion = testResourcesVersion; 149 this.classpathInference = classpathInference; 150 this.testResourcesDependencies = testResourcesDependencies; 151 this.sharedServerNamespace = sharedServerNamespace; 152 this.debugServer = debugServer; 153 this.foreground = foreground; 154 this.testResourcesSystemProperties = testResourcesSystemProperties; 155 } 156 157 public TestResourcesHelper(MavenSession mavenSession, boolean enabled, boolean shared, File buildDirectory) { 158 this.mavenSession = mavenSession; 159 this.enabled = enabled; 160 this.shared = shared; 161 this.buildDirectory = buildDirectory; 162 this.log = new SystemStreamLog(); 163 } 164 165 private boolean isKeepAlive() { 166 boolean hasKeepAliveFile = Files.isRegularFile(getKeepAliveFile(), LinkOption.NOFOLLOW_LINKS); 167 return hasKeepAliveFile || isStartExplicitlyInvoked(); 168 } 169 170 private boolean isStartExplicitlyInvoked() { 171 return mavenSession.getGoals() 172 .stream() 173 .anyMatch(goal -> goal.equals("mn:" + StartTestResourcesServerMojo.NAME)); 174 } 175 176 /** 177 * Starts the Test Resources Service. 178 */ 179 public void start() throws MojoExecutionException { 180 if (!enabled) { 181 return; 182 } 183 try { 184 doStart(); 185 } catch (Exception e) { 186 throw new MojoExecutionException("Unable to start test resources server", e); 187 } 188 } 189 190 private void doStart() throws IOException { 191 var accessToken = UUID.randomUUID().toString(); 192 Path buildDir = buildDirectory.toPath(); 193 Path serverSettingsDirectory = getServerSettingsDirectory(); 194 var serverStarted = new AtomicBoolean(false); 195 var serverFactory = new DefaultServerFactory(log, toolchainManager, mavenSession, serverStarted, testResourcesVersion, debugServer, foreground, testResourcesSystemProperties); 196 if (shared) { 197 try (var ignored = sharedServerLock(serverSettingsDirectory)) { 198 doStart(accessToken, buildDir, serverSettingsDirectory, serverFactory, serverStarted); 199 } 200 return; 201 } 202 doStart(accessToken, buildDir, serverSettingsDirectory, serverFactory, serverStarted); 203 } 204 205 private void doStart(String accessToken, 206 Path buildDir, 207 Path serverSettingsDirectory, 208 ServerFactory serverFactory, 209 AtomicBoolean serverStarted) throws IOException { 210 Optional<ServerSettings> optionalServerSettings = startOrConnectToExistingServer(accessToken, buildDir, serverSettingsDirectory, serverFactory); 211 if (optionalServerSettings.isEmpty()) { 212 return; 213 } 214 ServerSettings serverSettings = optionalServerSettings.get(); 215 writePortFile(buildDir.resolve(PORT_FILE_NAME), serverSettings.getPort()); 216 boolean sessionOwnedSharedServer = shared && registerSharedServerUse(serverSettingsDirectory, serverSettings.getPort(), serverStarted.get()); 217 if (shared) { 218 logSharedMode(serverSettingsDirectory); 219 writeSharedScopeConfiguration(); 220 } 221 setSystemProperties(serverSettings); 222 if (serverStarted.get()) { 223 if (isKeepAlive()) { 224 log.info("Micronaut Test Resources service is started in the background. To stop it, run the following command: 'mvn mn:" + StopTestResourcesServerMojo.NAME + "'"); 225 } 226 } else if (!sessionOwnedSharedServer) { 227 // A server was already listening before this build started, so leave it running. 228 createKeepAliveFile(); 229 } 230 } 231 232 /** 233 * Computes the system properties to set for the test resources client to be able to connect to the server. 234 * 235 * @param serverSettings The server settings 236 * @return The system properties 237 */ 238 public Map<String, String> computeSystemProperties(ServerSettings serverSettings) { 239 var systemProperties = new HashMap<String, String>(3); 240 String uri = String.format("http://localhost:%d", serverSettings.getPort()); 241 systemProperties.put(TEST_RESOURCES_PROP_SERVER_URI, uri); 242 serverSettings.getAccessToken().ifPresent(accessToken -> systemProperties.put(TEST_RESOURCES_PROP_ACCESS_TOKEN, accessToken)); 243 serverSettings.getClientTimeout().ifPresent(timeout -> systemProperties.put(TEST_RESOURCES_PROP_CLIENT_READ_TIMEOUT, String.valueOf(timeout))); 244 return systemProperties; 245 } 246 247 private void setSystemProperties(ServerSettings serverSettings) { 248 computeSystemProperties(serverSettings).forEach(System::setProperty); 249 } 250 251 private static void writePortFile(Path file, int port) throws IOException { 252 Files.createDirectories(file.getParent()); 253 Files.writeString(file, Integer.toString(port)); 254 } 255 256 private Optional<ServerSettings> startOrConnectToExistingServer(String accessToken, Path buildDir, Path serverSettingsDirectory, ServerFactory serverFactory) { 257 try { 258 return Optional.ofNullable( 259 ServerUtils.startOrConnectToExistingServer( 260 explicitPort, 261 buildDir.resolve(PORT_FILE_NAME), 262 serverSettingsDirectory, 263 accessToken, 264 resolveServerClasspath(), 265 clientTimeout, 266 serverIdleTimeoutMinutes, 267 serverFactory 268 ) 269 ); 270 } catch (Exception e) { 271 log.error("Error starting Micronaut Test Resources service", e); 272 return Optional.empty(); 273 } 274 } 275 276 private List<File> resolveServerClasspath() throws DependencyResolutionException { 277 List<MavenDependency> applicationDependencies = Collections.emptyList(); 278 if (classpathInference) { 279 applicationDependencies = getApplicationDependencies(); 280 } 281 Stream<Artifact> serverDependencies = 282 TestResourcesClasspath.inferTestResourcesClasspath(applicationDependencies, testResourcesVersion) 283 .stream() 284 .map(DependencyResolutionService::testResourcesDependencyToAetherArtifact); 285 286 List<org.apache.maven.model.Dependency> extraDependencies = 287 testResourcesDependencies != null ? testResourcesDependencies : Collections.emptyList(); 288 289 Stream<Artifact> extraDependenciesStream = extraDependencies.stream() 290 .map(DependencyResolutionService::mavenDependencyToAetherArtifact); 291 292 Stream<Artifact> artifacts = concat(serverDependencies, extraDependenciesStream); 293 294 var resolutionResult = dependencyResolutionService.artifactResultsFor(artifacts, true); 295 var filteredArtifacts = resolutionResult.stream() 296 .filter(result -> { 297 var artifact = result.getArtifact(); 298 var id = new ModuleIdentifier(artifact.getGroupId(), artifact.getArtifactId()); 299 return TestResourcesClasspath.isDependencyAllowedOnServerClasspath(id); 300 }) 301 .toList(); 302 return toClasspathFiles(filteredArtifacts); 303 } 304 305 private List<MavenDependency> getApplicationDependencies() { 306 return this.mavenProject.getDependencies().stream() 307 .map(DependencyResolutionService::mavenDependencyToTestResourcesDependency) 308 .toList(); 309 } 310 311 /** 312 * Contains the logic to stop the Test Resources Service. 313 * 314 * @param quiet Whether to perform logging or not. 315 */ 316 public void stop(boolean quiet) throws MojoExecutionException { 317 if (!enabled) { 318 return; 319 } 320 if (shared) { 321 try (var ignored = sharedServerLock(getServerSettingsDirectory())) { 322 stopSharedServer(quiet); 323 } 324 return; 325 } 326 stopServer(quiet); 327 } 328 329 private void stopSharedServer(boolean quiet) throws MojoExecutionException { 330 if (releaseSharedServerUse(getServerSettingsDirectory())) { 331 try { 332 cleanupSharedProjectSettings(); 333 } catch (IOException e) { 334 throw new MojoExecutionException("Unable to clean shared test resources settings", e); 335 } 336 log("Keeping Micronaut Test Resources service alive for another parallel reactor module", quiet); 337 return; 338 } 339 stopServer(quiet); 340 } 341 342 private void stopServer(boolean quiet) throws MojoExecutionException { 343 if (isKeepAlive()) { 344 log("Keeping Micronaut Test Resources service alive", quiet); 345 return; 346 } 347 try { 348 Optional<ServerSettings> optionalServerSettings = ServerUtils.readServerSettings(getServerSettingsDirectory()); 349 if (optionalServerSettings.isPresent()) { 350 if (isServerStarted(optionalServerSettings.get().getPort())) { 351 log("Shutting down Micronaut Test Resources service", quiet); 352 doStop(); 353 } else { 354 log("Cannot find Micronaut Test Resources service settings, server may already be shutdown", quiet); 355 Files.deleteIfExists(getServerSettingsDirectory().resolve(PROPERTIES_FILE_NAME)); 356 } 357 cleanupSharedProjectSettings(); 358 } 359 } catch (Exception e) { 360 var message = "Unable to stop test resources server"; 361 if (quiet) { 362 log.warn(message, e); 363 } else { 364 throw new MojoExecutionException(message, e); 365 } 366 } 367 } 368 369 private void logSharedMode(Path serverSettingsDirectory) throws IOException { 370 if (sharedServerNamespace != null) { 371 log.info("Test Resources is configured in shared mode with the namespace: " + sharedServerNamespace); 372 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 373 Files.createDirectories(projectSettingsDirectory); 374 Path source = serverSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 375 Path target = projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 376 Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); 377 } else { 378 log.info("Test Resources is configured in shared mode"); 379 } 380 } 381 382 private void writeSharedScopeConfiguration() throws IOException { 383 String scope = sharedScope(); 384 if (scope == null) { 385 return; 386 } 387 Path testClassesDirectory = testOutputDirectory(mavenProject, buildDirectory); 388 Files.createDirectories(testClassesDirectory); 389 updateApplicationTestProperties(testClassesDirectory.resolve(APPLICATION_TEST_PROPERTIES), scope); 390 log.info("Using Micronaut Test Resources scope " + scope + " for " + moduleKey()); 391 } 392 393 static Path testOutputDirectory(MavenProject mavenProject, File buildDirectory) { 394 if (mavenProject != null && mavenProject.getBuild() != null) { 395 String testOutputDirectory = mavenProject.getBuild().getTestOutputDirectory(); 396 if (testOutputDirectory != null && !testOutputDirectory.isBlank()) { 397 return Path.of(testOutputDirectory); 398 } 399 } 400 return buildDirectory.toPath().resolve("test-classes"); 401 } 402 403 static void updateApplicationTestProperties(Path file, String scope) throws IOException { 404 Properties properties = new Properties(); 405 if (Files.exists(file)) { 406 try (InputStream input = Files.newInputStream(file)) { 407 properties.load(input); 408 } 409 } 410 properties.setProperty(TEST_RESOURCES_SCOPE_PROPERTY, scope); 411 try (OutputStream output = Files.newOutputStream(file)) { 412 properties.store(output, "Generated by micronaut-maven-plugin"); 413 } 414 } 415 416 static String sanitizeScopeSegment(String value) { 417 String sanitized = value.replaceAll("[/\\\\]+", ".") 418 .replaceAll("[^A-Za-z0-9_.-]", "-") 419 .replaceAll("[.]{2,}", ".") 420 .replaceAll("-{2,}", "-"); 421 sanitized = trimScopeDelimiters(sanitized); 422 return sanitized.isEmpty() ? "root" : sanitized; 423 } 424 425 private static String trimScopeDelimiters(String value) { 426 int start = 0; 427 int end = value.length(); 428 while (start < end && isScopeDelimiter(value.charAt(start))) { 429 start++; 430 } 431 while (end > start && isScopeDelimiter(value.charAt(end - 1))) { 432 end--; 433 } 434 return value.substring(start, end); 435 } 436 437 private static boolean isScopeDelimiter(char c) { 438 return c == '.' || c == '-'; 439 } 440 441 private String sharedScope() { 442 if (!shared || mavenProject == null) { 443 return null; 444 } 445 Path multiModuleDirectory = mavenSession.getRequest().getMultiModuleProjectDirectory() == null 446 ? null 447 : mavenSession.getRequest().getMultiModuleProjectDirectory().toPath().toAbsolutePath().normalize(); 448 Path projectDirectory = mavenProject.getBasedir() == null 449 ? null 450 : mavenProject.getBasedir().toPath().toAbsolutePath().normalize(); 451 String projectSegment = mavenProject.getArtifactId(); 452 if (multiModuleDirectory != null && projectDirectory != null && projectDirectory.startsWith(multiModuleDirectory)) { 453 Path relativePath = multiModuleDirectory.relativize(projectDirectory); 454 if (relativePath.getNameCount() > 0) { 455 projectSegment = relativePath.toString(); 456 } 457 } 458 return sessionState().scopePrefix + "." + sanitizeScopeSegment(projectSegment); 459 } 460 461 private boolean registerSharedServerUse(Path serverSettingsDirectory, int port, boolean serverStarted) { 462 if (mavenProject == null) { 463 return false; 464 } 465 synchronized (SESSION_STATE_MONITOR) { 466 Path key = normalize(serverSettingsDirectory); 467 SessionState sessionState = sessionState(); 468 SharedServerState sharedServerState = sessionState.sharedServers.get(key); 469 if (serverStarted) { 470 sharedServerState = new SharedServerState(port); 471 sessionState.sharedServers.put(key, sharedServerState); 472 } else if (sharedServerState == null || sharedServerState.port != port) { 473 return false; 474 } 475 sharedServerState.owners.add(moduleKey()); 476 return true; 477 } 478 } 479 480 private boolean releaseSharedServerUse(Path serverSettingsDirectory) { 481 if (mavenProject == null) { 482 return false; 483 } 484 synchronized (SESSION_STATE_MONITOR) { 485 SessionState sessionState = SESSION_STATES.get(mavenSession); 486 if (sessionState == null) { 487 return false; 488 } 489 SharedServerState sharedServerState = sessionState.sharedServers.get(normalize(serverSettingsDirectory)); 490 if (sharedServerState == null) { 491 return false; 492 } 493 sharedServerState.owners.remove(moduleKey()); 494 if (!sharedServerState.owners.isEmpty()) { 495 return true; 496 } 497 sessionState.sharedServers.remove(normalize(serverSettingsDirectory)); 498 return false; 499 } 500 } 501 502 private SessionState sessionState() { 503 synchronized (SESSION_STATE_MONITOR) { 504 return SESSION_STATES.computeIfAbsent(mavenSession, ignored -> new SessionState()); 505 } 506 } 507 508 private String moduleKey() { 509 if (mavenProject == null || mavenProject.getBasedir() == null) { 510 return buildDirectory.toPath().toAbsolutePath().normalize().toString(); 511 } 512 return mavenProject.getBasedir().toPath().toAbsolutePath().normalize().toString(); 513 } 514 515 private static Path normalize(Path path) { 516 return path.toAbsolutePath().normalize(); 517 } 518 519 static SharedServerLock sharedServerLock(Path serverSettingsDirectory) { 520 Path key = normalize(serverSettingsDirectory); 521 ReentrantLock lock = SHARED_SERVER_LOCKS.computeIfAbsent(key, ignored -> new ReentrantLock()); 522 return new SharedServerLock(key, lock); 523 } 524 525 static int sharedServerLockCount() { 526 return SHARED_SERVER_LOCKS.size(); 527 } 528 529 private static void releaseSharedServerLock(Path key, ReentrantLock lock) { 530 lock.unlock(); 531 if (!lock.isLocked() && !lock.hasQueuedThreads()) { 532 SHARED_SERVER_LOCKS.remove(key, lock); 533 } 534 } 535 536 private void createKeepAliveFile() throws IOException { 537 Path keepalive = getKeepAliveFile(); 538 createKeepAliveDirectory(); 539 if (Files.exists(keepalive, LinkOption.NOFOLLOW_LINKS)) { 540 if (!Files.isRegularFile(keepalive, LinkOption.NOFOLLOW_LINKS)) { 541 throw new IOException("Keepalive path exists but is not a regular file: " + keepalive); 542 } 543 } else { 544 Files.writeString(keepalive, "true", StandardOpenOption.CREATE_NEW); 545 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 546 try { 547 deleteKeepAliveFile(); 548 } catch (MojoExecutionException e) { 549 // ignore, we're in a shutdown hook 550 } 551 })); 552 } 553 } 554 555 private void createKeepAliveDirectory() throws IOException { 556 synchronized (SESSION_STATE_MONITOR) { 557 Path keepAliveDirectory = sessionState().keepAliveDirectory; 558 if (Files.exists(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 559 if (!Files.isDirectory(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 560 throw new IOException("Keepalive directory exists but is not a directory: " + keepAliveDirectory); 561 } 562 return; 563 } 564 try { 565 FileAttribute<?>[] attributes = keepAliveDirectoryAttributes(keepAliveDirectory.getParent()); 566 if (attributes.length == 0) { 567 Files.createDirectory(keepAliveDirectory); 568 } else { 569 Files.createDirectory(keepAliveDirectory, attributes); 570 } 571 } catch (FileAlreadyExistsException e) { 572 if (!Files.isDirectory(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 573 throw new IOException("Keepalive directory exists but is not a directory: " + keepAliveDirectory, e); 574 } 575 } 576 } 577 } 578 579 private static boolean isServerStarted(int port) { 580 if (System.getProperty("test.resources.internal.server.started") != null) { 581 return Boolean.getBoolean("test.resources.internal.server.started"); 582 } else { 583 return !SocketUtils.isTcpPortAvailable(port); 584 } 585 } 586 587 private void log(String message, boolean quiet) { 588 if (quiet) { 589 if (log.isDebugEnabled()) { 590 log.debug(message); 591 } 592 } else { 593 log.info(message); 594 } 595 } 596 597 private void doStop() throws IOException, MojoExecutionException { 598 try { 599 Path settingsDirectory = getServerSettingsDirectory(); 600 ServerUtils.stopServer(settingsDirectory); 601 } finally { 602 deleteKeepAliveFile(); 603 } 604 } 605 606 private void deleteKeepAliveFile() throws MojoExecutionException { 607 if (Files.exists(getKeepAliveFile(), LinkOption.NOFOLLOW_LINKS)) { 608 try { 609 Files.delete(getKeepAliveFile()); 610 } catch (IOException e) { 611 throw new MojoExecutionException("Failed to delete keepalive file", e); 612 } 613 } 614 tryDeleteKeepAliveDirectory(); 615 } 616 617 private Path getServerSettingsDirectory() { 618 if (shared) { 619 return ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace); 620 } 621 return serverSettingsDirectoryOf(buildDirectory.toPath()); 622 } 623 624 private Path getKeepAliveFile() { 625 return sessionState().keepAliveDirectory.resolve("keepalive-" + mavenSession.getRequest().getBuilderId()); 626 } 627 628 private void tryDeleteKeepAliveDirectory() throws MojoExecutionException { 629 Path keepAliveDirectory = sessionState().keepAliveDirectory; 630 try { 631 Files.deleteIfExists(keepAliveDirectory); 632 } catch (DirectoryNotEmptyException ignored) { 633 // Another keepalive file in the same session still owns the directory. 634 } catch (IOException e) { 635 throw new MojoExecutionException("Failed to delete keepalive directory", e); 636 } 637 } 638 639 @SuppressWarnings("unchecked") 640 private static FileAttribute<?>[] keepAliveDirectoryAttributes(Path directory) { 641 try { 642 if (directory == null || !Files.getFileStore(directory).supportsFileAttributeView("posix")) { 643 return new FileAttribute[0]; 644 } 645 return new FileAttribute<?>[]{ 646 PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")) 647 }; 648 } catch (IOException | UnsupportedOperationException | SecurityException e) { 649 return new FileAttribute[0]; 650 } 651 } 652 653 private void cleanupSharedProjectSettings() throws IOException { 654 if (shared && sharedServerNamespace != null) { 655 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 656 Files.deleteIfExists(projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES)); 657 } 658 } 659 660 private Path serverSettingsDirectoryOf(Path buildDir) { 661 return buildDir.resolve("../.micronaut/test-resources"); 662 } 663 664 /** 665 * @param sharedServerNamespace The shared server namespace (if any). 666 */ 667 public void setSharedServerNamespace(String sharedServerNamespace) { 668 this.sharedServerNamespace = sharedServerNamespace; 669 } 670 671 private static final class SessionState { 672 private final String scopePrefix = SCOPE_PREFIX + "-" + UUID.randomUUID(); 673 private final Path keepAliveDirectory = Path.of(System.getProperty("java.io.tmpdir")) 674 .resolve("mn-test-resources-" + UUID.randomUUID()); 675 private final Map<Path, SharedServerState> sharedServers = new LinkedHashMap<>(); 676 } 677 678 private static final class SharedServerState { 679 private final int port; 680 private final Set<String> owners = new HashSet<>(); 681 682 private SharedServerState(int port) { 683 this.port = port; 684 } 685 } 686 687 static final class SharedServerLock implements AutoCloseable { 688 private final Path key; 689 private final ReentrantLock lock; 690 691 private SharedServerLock(Path key, ReentrantLock lock) { 692 this.key = key; 693 this.lock = lock; 694 this.lock.lock(); 695 } 696 697 @Override 698 public void close() { 699 releaseSharedServerLock(key, lock); 700 } 701 } 702}