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