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