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}