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, !isKeepAlive(), 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> sessionSharedServerSettings = findSessionSharedServer(serverSettingsDirectory); 211 if (sessionSharedServerSettings.isPresent()) { 212 ServerSettings serverSettings = sessionSharedServerSettings.get(); 213 writePortFile(buildDir.resolve(PORT_FILE_NAME), serverSettings.getPort()); 214 registerSharedServerUse(serverSettingsDirectory, serverSettings.getPort(), false); 215 logSharedMode(serverSettingsDirectory); 216 writeSharedScopeConfiguration(); 217 setSystemProperties(serverSettings); 218 return; 219 } 220 Optional<ServerSettings> optionalServerSettings = startOrConnectToExistingServer(accessToken, buildDir, serverSettingsDirectory, serverFactory); 221 if (optionalServerSettings.isEmpty()) { 222 return; 223 } 224 ServerSettings serverSettings = optionalServerSettings.get(); 225 writePortFile(buildDir.resolve(PORT_FILE_NAME), serverSettings.getPort()); 226 boolean sessionOwnedSharedServer = shared && registerSharedServerUse(serverSettingsDirectory, serverSettings.getPort(), serverStarted.get()); 227 if (shared) { 228 logSharedMode(serverSettingsDirectory); 229 writeSharedScopeConfiguration(); 230 } 231 setSystemProperties(serverSettings); 232 if (serverStarted.get()) { 233 if (isKeepAlive()) { 234 log.info("Micronaut Test Resources service is started in the background. To stop it, run the following command: 'mvn mn:" + StopTestResourcesServerMojo.NAME + "'"); 235 } 236 } else if (!sessionOwnedSharedServer) { 237 // A server was already listening before this build started, so leave it running. 238 createKeepAliveFile(); 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 /** 261 * Computes the system properties to set for the test resources client to be able to connect to the server. 262 * 263 * @param serverSettings The server settings 264 * @return The system properties 265 */ 266 public Map<String, String> computeSystemProperties(ServerSettings serverSettings) { 267 var systemProperties = new HashMap<String, String>(3); 268 String uri = String.format("http://localhost:%d", serverSettings.getPort()); 269 systemProperties.put(TEST_RESOURCES_PROP_SERVER_URI, uri); 270 serverSettings.getAccessToken().ifPresent(accessToken -> systemProperties.put(TEST_RESOURCES_PROP_ACCESS_TOKEN, accessToken)); 271 serverSettings.getClientTimeout().ifPresent(timeout -> systemProperties.put(TEST_RESOURCES_PROP_CLIENT_READ_TIMEOUT, String.valueOf(timeout))); 272 return systemProperties; 273 } 274 275 private void setSystemProperties(ServerSettings serverSettings) { 276 computeSystemProperties(serverSettings).forEach(System::setProperty); 277 } 278 279 private static void writePortFile(Path file, int port) throws IOException { 280 Files.createDirectories(file.getParent()); 281 Files.writeString(file, Integer.toString(port)); 282 } 283 284 private Optional<ServerSettings> startOrConnectToExistingServer(String accessToken, Path buildDir, Path serverSettingsDirectory, ServerFactory serverFactory) { 285 try { 286 return Optional.ofNullable( 287 ServerUtils.startOrConnectToExistingServer( 288 explicitPort, 289 buildDir.resolve(PORT_FILE_NAME), 290 serverSettingsDirectory, 291 accessToken, 292 resolveServerClasspath(), 293 clientTimeout, 294 serverIdleTimeoutMinutes, 295 serverFactory 296 ) 297 ); 298 } catch (Exception e) { 299 log.error("Error starting Micronaut Test Resources service", e); 300 return Optional.empty(); 301 } 302 } 303 304 private List<File> resolveServerClasspath() throws DependencyResolutionException { 305 List<MavenDependency> applicationDependencies = Collections.emptyList(); 306 if (classpathInference) { 307 applicationDependencies = getApplicationDependencies(); 308 } 309 Stream<Artifact> serverDependencies = 310 TestResourcesClasspath.inferTestResourcesClasspath(applicationDependencies, testResourcesVersion) 311 .stream() 312 .map(DependencyResolutionService::testResourcesDependencyToAetherArtifact); 313 314 List<org.apache.maven.model.Dependency> extraDependencies = 315 testResourcesDependencies != null ? testResourcesDependencies : Collections.emptyList(); 316 317 Stream<Artifact> extraDependenciesStream = extraDependencies.stream() 318 .map(DependencyResolutionService::mavenDependencyToAetherArtifact); 319 320 Stream<Artifact> artifacts = concat(serverDependencies, extraDependenciesStream); 321 322 var resolutionResult = dependencyResolutionService.artifactResultsFor(artifacts, true); 323 var filteredArtifacts = resolutionResult.stream() 324 .filter(result -> { 325 var artifact = result.getArtifact(); 326 var id = new ModuleIdentifier(artifact.getGroupId(), artifact.getArtifactId()); 327 return TestResourcesClasspath.isDependencyAllowedOnServerClasspath(id); 328 }) 329 .toList(); 330 return toClasspathFiles(filteredArtifacts); 331 } 332 333 private List<MavenDependency> getApplicationDependencies() { 334 return this.mavenProject.getDependencies().stream() 335 .map(DependencyResolutionService::mavenDependencyToTestResourcesDependency) 336 .toList(); 337 } 338 339 /** 340 * Contains the logic to stop the Test Resources Service. 341 * 342 * @param quiet Whether to perform logging or not. 343 */ 344 public void stop(boolean quiet) throws MojoExecutionException { 345 if (!enabled) { 346 return; 347 } 348 if (shared) { 349 try (var ignored = sharedServerLock(getServerSettingsDirectory())) { 350 stopSharedServer(quiet); 351 } 352 return; 353 } 354 stopServer(quiet); 355 } 356 357 private void stopSharedServer(boolean quiet) throws MojoExecutionException { 358 if (releaseSharedServerUse(getServerSettingsDirectory())) { 359 try { 360 cleanupSharedProjectSettings(); 361 } catch (IOException e) { 362 throw new MojoExecutionException("Unable to clean shared test resources settings", e); 363 } 364 log("Keeping Micronaut Test Resources service alive for another parallel reactor module", quiet); 365 return; 366 } 367 stopServer(quiet); 368 } 369 370 private void stopServer(boolean quiet) throws MojoExecutionException { 371 if (isKeepAlive()) { 372 log("Keeping Micronaut Test Resources service alive", quiet); 373 return; 374 } 375 try { 376 Optional<ServerSettings> optionalServerSettings = ServerUtils.readServerSettings(getServerSettingsDirectory()); 377 if (optionalServerSettings.isPresent()) { 378 if (isServerStarted(optionalServerSettings.get().getPort())) { 379 log("Shutting down Micronaut Test Resources service", quiet); 380 doStop(); 381 } else { 382 log("Cannot find Micronaut Test Resources service settings, server may already be shutdown", quiet); 383 Files.deleteIfExists(getServerSettingsDirectory().resolve(PROPERTIES_FILE_NAME)); 384 } 385 cleanupSharedProjectSettings(); 386 } 387 } catch (Exception e) { 388 var message = "Unable to stop test resources server"; 389 if (quiet) { 390 log.warn(message, e); 391 } else { 392 throw new MojoExecutionException(message, e); 393 } 394 } 395 } 396 397 private void logSharedMode(Path serverSettingsDirectory) throws IOException { 398 if (sharedServerNamespace != null) { 399 log.info("Test Resources is configured in shared mode with the namespace: " + sharedServerNamespace); 400 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 401 Files.createDirectories(projectSettingsDirectory); 402 Path source = serverSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 403 Path target = projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 404 Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); 405 } else { 406 log.info("Test Resources is configured in shared mode"); 407 } 408 } 409 410 private void writeSharedScopeConfiguration() throws IOException { 411 String scope = sharedScope(); 412 if (scope == null) { 413 return; 414 } 415 Path testClassesDirectory = testOutputDirectory(mavenProject, buildDirectory); 416 Files.createDirectories(testClassesDirectory); 417 updateApplicationTestProperties(testClassesDirectory.resolve(APPLICATION_TEST_PROPERTIES), scope); 418 log.info("Using Micronaut Test Resources scope " + scope + " for " + moduleKey()); 419 } 420 421 static Path testOutputDirectory(MavenProject mavenProject, File buildDirectory) { 422 if (mavenProject != null && mavenProject.getBuild() != null) { 423 String testOutputDirectory = mavenProject.getBuild().getTestOutputDirectory(); 424 if (testOutputDirectory != null && !testOutputDirectory.isBlank()) { 425 return Path.of(testOutputDirectory); 426 } 427 } 428 return buildDirectory.toPath().resolve("test-classes"); 429 } 430 431 static void updateApplicationTestProperties(Path file, String scope) throws IOException { 432 Properties properties = new Properties(); 433 if (Files.exists(file)) { 434 try (InputStream input = Files.newInputStream(file)) { 435 properties.load(input); 436 } 437 } 438 properties.setProperty(TEST_RESOURCES_SCOPE_PROPERTY, scope); 439 try (OutputStream output = Files.newOutputStream(file)) { 440 properties.store(output, "Generated by micronaut-maven-plugin"); 441 } 442 } 443 444 static String sanitizeScopeSegment(String value) { 445 String sanitized = value.replaceAll("[/\\\\]+", ".") 446 .replaceAll("[^A-Za-z0-9_.-]", "-") 447 .replaceAll("[.]{2,}", ".") 448 .replaceAll("-{2,}", "-"); 449 sanitized = trimScopeDelimiters(sanitized); 450 return sanitized.isEmpty() ? "root" : sanitized; 451 } 452 453 private static String trimScopeDelimiters(String value) { 454 int start = 0; 455 int end = value.length(); 456 while (start < end && isScopeDelimiter(value.charAt(start))) { 457 start++; 458 } 459 while (end > start && isScopeDelimiter(value.charAt(end - 1))) { 460 end--; 461 } 462 return value.substring(start, end); 463 } 464 465 private static boolean isScopeDelimiter(char c) { 466 return c == '.' || c == '-'; 467 } 468 469 private String sharedScope() { 470 if (!shared || mavenProject == null) { 471 return null; 472 } 473 Path multiModuleDirectory = mavenSession.getRequest().getMultiModuleProjectDirectory() == null 474 ? null 475 : mavenSession.getRequest().getMultiModuleProjectDirectory().toPath().toAbsolutePath().normalize(); 476 Path projectDirectory = mavenProject.getBasedir() == null 477 ? null 478 : mavenProject.getBasedir().toPath().toAbsolutePath().normalize(); 479 String projectSegment = mavenProject.getArtifactId(); 480 if (multiModuleDirectory != null && projectDirectory != null && projectDirectory.startsWith(multiModuleDirectory)) { 481 Path relativePath = multiModuleDirectory.relativize(projectDirectory); 482 if (relativePath.getNameCount() > 0) { 483 projectSegment = relativePath.toString(); 484 } 485 } 486 return sessionState().scopePrefix + "." + sanitizeScopeSegment(projectSegment); 487 } 488 489 private boolean registerSharedServerUse(Path serverSettingsDirectory, int port, boolean serverStarted) { 490 if (mavenProject == null) { 491 return false; 492 } 493 synchronized (SESSION_STATE_MONITOR) { 494 Path key = normalize(serverSettingsDirectory); 495 SessionState sessionState = sessionState(); 496 SharedServerState sharedServerState = sessionState.sharedServers.get(key); 497 if (serverStarted) { 498 sharedServerState = new SharedServerState(port); 499 sessionState.sharedServers.put(key, sharedServerState); 500 } else if (sharedServerState == null || sharedServerState.port != port) { 501 return false; 502 } 503 sharedServerState.owners.add(moduleKey()); 504 return true; 505 } 506 } 507 508 private boolean releaseSharedServerUse(Path serverSettingsDirectory) { 509 if (mavenProject == null) { 510 return false; 511 } 512 synchronized (SESSION_STATE_MONITOR) { 513 SessionState sessionState = SESSION_STATES.get(mavenSession); 514 if (sessionState == null) { 515 return false; 516 } 517 SharedServerState sharedServerState = sessionState.sharedServers.get(normalize(serverSettingsDirectory)); 518 if (sharedServerState == null) { 519 return false; 520 } 521 sharedServerState.owners.remove(moduleKey()); 522 if (!sharedServerState.owners.isEmpty()) { 523 return true; 524 } 525 sessionState.sharedServers.remove(normalize(serverSettingsDirectory)); 526 return false; 527 } 528 } 529 530 private SessionState sessionState() { 531 synchronized (SESSION_STATE_MONITOR) { 532 return SESSION_STATES.computeIfAbsent(mavenSession, ignored -> new SessionState()); 533 } 534 } 535 536 private String moduleKey() { 537 if (mavenProject == null || mavenProject.getBasedir() == null) { 538 return buildDirectory.toPath().toAbsolutePath().normalize().toString(); 539 } 540 return mavenProject.getBasedir().toPath().toAbsolutePath().normalize().toString(); 541 } 542 543 private static Path normalize(Path path) { 544 return path.toAbsolutePath().normalize(); 545 } 546 547 static SharedServerLock sharedServerLock(Path serverSettingsDirectory) { 548 Path key = normalize(serverSettingsDirectory); 549 ReentrantLock lock = SHARED_SERVER_LOCKS.computeIfAbsent(key, ignored -> new ReentrantLock()); 550 return new SharedServerLock(key, lock); 551 } 552 553 static int sharedServerLockCount() { 554 return SHARED_SERVER_LOCKS.size(); 555 } 556 557 private static void releaseSharedServerLock(Path key, ReentrantLock lock) { 558 lock.unlock(); 559 if (!lock.isLocked() && !lock.hasQueuedThreads()) { 560 SHARED_SERVER_LOCKS.remove(key, lock); 561 } 562 } 563 564 private void createKeepAliveFile() throws IOException { 565 Path keepalive = getKeepAliveFile(); 566 createKeepAliveDirectory(); 567 if (Files.exists(keepalive, LinkOption.NOFOLLOW_LINKS)) { 568 if (!Files.isRegularFile(keepalive, LinkOption.NOFOLLOW_LINKS)) { 569 throw new IOException("Keepalive path exists but is not a regular file: " + keepalive); 570 } 571 } else { 572 Files.writeString(keepalive, "true", StandardOpenOption.CREATE_NEW); 573 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 574 try { 575 deleteKeepAliveFile(); 576 } catch (MojoExecutionException e) { 577 // ignore, we're in a shutdown hook 578 } 579 })); 580 } 581 } 582 583 private void createKeepAliveDirectory() throws IOException { 584 synchronized (SESSION_STATE_MONITOR) { 585 Path keepAliveDirectory = sessionState().keepAliveDirectory; 586 if (Files.exists(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 587 if (!Files.isDirectory(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 588 throw new IOException("Keepalive directory exists but is not a directory: " + keepAliveDirectory); 589 } 590 return; 591 } 592 try { 593 FileAttribute<?>[] attributes = keepAliveDirectoryAttributes(keepAliveDirectory.getParent()); 594 if (attributes.length == 0) { 595 Files.createDirectory(keepAliveDirectory); 596 } else { 597 Files.createDirectory(keepAliveDirectory, attributes); 598 } 599 } catch (FileAlreadyExistsException e) { 600 if (!Files.isDirectory(keepAliveDirectory, LinkOption.NOFOLLOW_LINKS)) { 601 throw new IOException("Keepalive directory exists but is not a directory: " + keepAliveDirectory, e); 602 } 603 } 604 } 605 } 606 607 private static boolean isServerStarted(int port) { 608 if (System.getProperty("test.resources.internal.server.started") != null) { 609 return Boolean.getBoolean("test.resources.internal.server.started"); 610 } else { 611 return !SocketUtils.isTcpPortAvailable(port); 612 } 613 } 614 615 private void log(String message, boolean quiet) { 616 if (quiet) { 617 if (log.isDebugEnabled()) { 618 log.debug(message); 619 } 620 } else { 621 log.info(message); 622 } 623 } 624 625 private void doStop() throws IOException, MojoExecutionException { 626 try { 627 Path settingsDirectory = getServerSettingsDirectory(); 628 ServerUtils.stopServer(settingsDirectory); 629 } finally { 630 deleteKeepAliveFile(); 631 } 632 } 633 634 private void deleteKeepAliveFile() throws MojoExecutionException { 635 if (Files.exists(getKeepAliveFile(), LinkOption.NOFOLLOW_LINKS)) { 636 try { 637 Files.delete(getKeepAliveFile()); 638 } catch (IOException e) { 639 throw new MojoExecutionException("Failed to delete keepalive file", e); 640 } 641 } 642 tryDeleteKeepAliveDirectory(); 643 } 644 645 private Path getServerSettingsDirectory() { 646 if (shared) { 647 return ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace); 648 } 649 return serverSettingsDirectoryOf(buildDirectory.toPath()); 650 } 651 652 private Path getKeepAliveFile() { 653 return sessionState().keepAliveDirectory.resolve("keepalive-" + mavenSession.getRequest().getBuilderId()); 654 } 655 656 private void tryDeleteKeepAliveDirectory() throws MojoExecutionException { 657 Path keepAliveDirectory = sessionState().keepAliveDirectory; 658 try { 659 Files.deleteIfExists(keepAliveDirectory); 660 } catch (DirectoryNotEmptyException ignored) { 661 // Another keepalive file in the same session still owns the directory. 662 } catch (IOException e) { 663 throw new MojoExecutionException("Failed to delete keepalive directory", e); 664 } 665 } 666 667 @SuppressWarnings("unchecked") 668 private static FileAttribute<?>[] keepAliveDirectoryAttributes(Path directory) { 669 try { 670 if (directory == null || !Files.getFileStore(directory).supportsFileAttributeView("posix")) { 671 return new FileAttribute[0]; 672 } 673 return new FileAttribute<?>[]{ 674 PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")) 675 }; 676 } catch (IOException | UnsupportedOperationException | SecurityException e) { 677 return new FileAttribute[0]; 678 } 679 } 680 681 private void cleanupSharedProjectSettings() throws IOException { 682 if (shared && sharedServerNamespace != null) { 683 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 684 Files.deleteIfExists(projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES)); 685 } 686 } 687 688 private Path serverSettingsDirectoryOf(Path buildDir) { 689 return buildDir.resolve("../.micronaut/test-resources"); 690 } 691 692 /** 693 * @param sharedServerNamespace The shared server namespace (if any). 694 */ 695 public void setSharedServerNamespace(String sharedServerNamespace) { 696 this.sharedServerNamespace = sharedServerNamespace; 697 } 698 699 private static final class SessionState { 700 private final String scopePrefix = SCOPE_PREFIX + "-" + UUID.randomUUID(); 701 private final Path keepAliveDirectory = Path.of(System.getProperty("java.io.tmpdir")) 702 .resolve("mn-test-resources-" + UUID.randomUUID()); 703 private final Map<Path, SharedServerState> sharedServers = new LinkedHashMap<>(); 704 } 705 706 private static final class SharedServerState { 707 private final int port; 708 private final Set<String> owners = new HashSet<>(); 709 710 private SharedServerState(int port) { 711 this.port = port; 712 } 713 } 714 715 static final class SharedServerLock implements AutoCloseable { 716 private final Path key; 717 private final ReentrantLock lock; 718 719 private SharedServerLock(Path key, ReentrantLock lock) { 720 this.key = key; 721 this.lock = lock; 722 this.lock.lock(); 723 } 724 725 @Override 726 public void close() { 727 releaseSharedServerLock(key, lock); 728 } 729 } 730}