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}