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