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.nio.file.Files; 039import java.nio.file.Path; 040import java.nio.file.StandardCopyOption; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.Optional; 046import java.util.UUID; 047import java.util.concurrent.atomic.AtomicBoolean; 048import java.util.stream.Stream; 049 050import static io.micronaut.maven.services.DependencyResolutionService.toClasspathFiles; 051import static io.micronaut.testresources.buildtools.ServerUtils.PROPERTIES_FILE_NAME; 052import static java.util.stream.Stream.concat; 053 054/** 055 * Utility class to stop Test Resources service. 056 */ 057public class TestResourcesHelper { 058 059 private static final String TEST_RESOURCES_PROPERTIES = "test-resources.properties"; 060 private static final String PORT_FILE_NAME = "test-resources-port.txt"; 061 062 private static final String TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX = "micronaut.test.resources."; 063 064 private static final String TEST_RESOURCES_PROP_SERVER_URI = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.uri"; 065 private static final String TEST_RESOURCES_PROP_ACCESS_TOKEN = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.access.token"; 066 private static final String TEST_RESOURCES_PROP_CLIENT_READ_TIMEOUT = TEST_RESOURCES_CLIENT_SYSTEM_PROP_PREFIX + "server.client.read.timeout"; 067 068 private final boolean enabled; 069 070 private final MavenSession mavenSession; 071 072 private final boolean shared; 073 074 private final File buildDirectory; 075 076 private final Log log; 077 078 private Integer explicitPort; 079 080 private Integer clientTimeout; 081 082 private Integer serverIdleTimeoutMinutes; 083 084 private MavenProject mavenProject; 085 086 private DependencyResolutionService dependencyResolutionService; 087 088 private ToolchainManager toolchainManager; 089 090 private String testResourcesVersion; 091 092 private boolean classpathInference; 093 094 private List<Dependency> testResourcesDependencies; 095 096 private String sharedServerNamespace; 097 098 private boolean debugServer; 099 100 private boolean foreground = false; 101 102 public TestResourcesHelper(boolean enabled, 103 boolean shared, 104 File buildDirectory, 105 Integer explicitPort, 106 Integer clientTimeout, 107 Integer serverIdleTimeoutMinutes, 108 MavenProject mavenProject, 109 MavenSession mavenSession, 110 DependencyResolutionService dependencyResolutionService, 111 ToolchainManager toolchainManager, 112 String testResourcesVersion, 113 boolean classpathInference, 114 List<Dependency> testResourcesDependencies, 115 String sharedServerNamespace, 116 boolean debugServer, 117 boolean foreground) { 118 this(mavenSession, enabled, shared, buildDirectory); 119 this.explicitPort = explicitPort; 120 this.clientTimeout = clientTimeout; 121 this.serverIdleTimeoutMinutes = serverIdleTimeoutMinutes; 122 this.mavenProject = mavenProject; 123 this.dependencyResolutionService = dependencyResolutionService; 124 this.toolchainManager = toolchainManager; 125 this.testResourcesVersion = testResourcesVersion; 126 this.classpathInference = classpathInference; 127 this.testResourcesDependencies = testResourcesDependencies; 128 this.sharedServerNamespace = sharedServerNamespace; 129 this.debugServer = debugServer; 130 this.foreground = foreground; 131 } 132 133 public TestResourcesHelper(MavenSession mavenSession, boolean enabled, boolean shared, File buildDirectory) { 134 this.mavenSession = mavenSession; 135 this.enabled = enabled; 136 this.shared = shared; 137 this.buildDirectory = buildDirectory; 138 this.log = new SystemStreamLog(); 139 } 140 141 private boolean isKeepAlive() { 142 boolean hasKeepAliveFile = Files.exists(getKeepAliveFile()); 143 return hasKeepAliveFile || isStartExplicitlyInvoked(); 144 } 145 146 private boolean isStartExplicitlyInvoked() { 147 return mavenSession.getGoals() 148 .stream() 149 .anyMatch(goal -> goal.equals("mn:" + StartTestResourcesServerMojo.NAME)); 150 } 151 152 /** 153 * Starts the Test Resources Service. 154 */ 155 public void start() throws MojoExecutionException { 156 if (!enabled) { 157 return; 158 } 159 try { 160 doStart(); 161 } catch (Exception e) { 162 throw new MojoExecutionException("Unable to start test resources server", e); 163 } 164 } 165 166 private void doStart() throws IOException { 167 var accessToken = UUID.randomUUID().toString(); 168 Path buildDir = buildDirectory.toPath(); 169 Path serverSettingsDirectory = getServerSettingsDirectory(); 170 var serverStarted = new AtomicBoolean(false); 171 var serverFactory = new DefaultServerFactory(log, toolchainManager, mavenSession, serverStarted, testResourcesVersion, debugServer, foreground); 172 Optional<ServerSettings> optionalServerSettings = startOrConnectToExistingServer(accessToken, buildDir, serverSettingsDirectory, serverFactory); 173 if (optionalServerSettings.isPresent()) { 174 ServerSettings serverSettings = optionalServerSettings.get(); 175 if (shared) { 176 if (sharedServerNamespace != null) { 177 log.info("Test Resources is configured in shared mode with the namespace: " + sharedServerNamespace); 178 //Copy the server settings to the default location so that TR Client can find it 179 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 180 Files.createDirectories(projectSettingsDirectory); 181 182 Path source = serverSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 183 Path target = projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES); 184 Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); 185 186 } else { 187 log.info("Test Resources is configured in shared mode"); 188 } 189 } 190 setSystemProperties(serverSettings); 191 if (serverStarted.get()) { 192 if (isKeepAlive()) { 193 log.info("Micronaut Test Resources service is started in the background. To stop it, run the following command: 'mvn mn:" + StopTestResourcesServerMojo.NAME + "'"); 194 } 195 } else { 196 // A server was already listening, which means it was running before 197 // the build was started, so we put a file to indicate to the stop 198 // mojo that it should not stop the server. 199 Path keepalive = getKeepAliveFile(); 200 // Test is because we may be running in watch mode 201 if (!Files.exists(keepalive)) { 202 Files.write(keepalive, "true".getBytes()); 203 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 204 // Make sure that if the build is interrupted, e.g using CTRL+C, the keepalive file is deleted 205 try { 206 deleteKeepAliveFile(); 207 } catch (MojoExecutionException e) { 208 // ignore, we're in a shutdown hook 209 } 210 })); 211 } 212 } 213 } 214 } 215 216 /** 217 * Computes the system properties to set for the test resources client to be able to connect to the server. 218 * 219 * @param serverSettings The server settings 220 * @return The system properties 221 */ 222 public Map<String, String> computeSystemProperties(ServerSettings serverSettings) { 223 var systemProperties = new HashMap<String, String>(3); 224 String uri = String.format("http://localhost:%d", serverSettings.getPort()); 225 systemProperties.put(TEST_RESOURCES_PROP_SERVER_URI, uri); 226 serverSettings.getAccessToken().ifPresent(accessToken -> systemProperties.put(TEST_RESOURCES_PROP_ACCESS_TOKEN, accessToken)); 227 serverSettings.getClientTimeout().ifPresent(timeout -> systemProperties.put(TEST_RESOURCES_PROP_CLIENT_READ_TIMEOUT, String.valueOf(timeout))); 228 return systemProperties; 229 } 230 231 private void setSystemProperties(ServerSettings serverSettings) { 232 computeSystemProperties(serverSettings).forEach(System::setProperty); 233 } 234 235 private Optional<ServerSettings> startOrConnectToExistingServer(String accessToken, Path buildDir, Path serverSettingsDirectory, ServerFactory serverFactory) { 236 try { 237 return Optional.ofNullable( 238 ServerUtils.startOrConnectToExistingServer( 239 explicitPort, 240 buildDir.resolve(PORT_FILE_NAME), 241 serverSettingsDirectory, 242 accessToken, 243 resolveServerClasspath(), 244 clientTimeout, 245 serverIdleTimeoutMinutes, 246 serverFactory 247 ) 248 ); 249 } catch (Exception e) { 250 log.error("Error starting Micronaut Test Resources service", e); 251 return Optional.empty(); 252 } 253 } 254 255 private List<File> resolveServerClasspath() throws DependencyResolutionException { 256 List<MavenDependency> applicationDependencies = Collections.emptyList(); 257 if (classpathInference) { 258 applicationDependencies = getApplicationDependencies(); 259 } 260 Stream<Artifact> serverDependencies = 261 TestResourcesClasspath.inferTestResourcesClasspath(applicationDependencies, testResourcesVersion) 262 .stream() 263 .map(DependencyResolutionService::testResourcesDependencyToAetherArtifact); 264 265 List<org.apache.maven.model.Dependency> extraDependencies = 266 testResourcesDependencies != null ? testResourcesDependencies : Collections.emptyList(); 267 268 Stream<Artifact> extraDependenciesStream = extraDependencies.stream() 269 .map(DependencyResolutionService::mavenDependencyToAetherArtifact); 270 271 Stream<Artifact> artifacts = concat(serverDependencies, extraDependenciesStream); 272 273 var resolutionResult = dependencyResolutionService.artifactResultsFor(artifacts, true); 274 var filteredArtifacts = resolutionResult.stream() 275 .filter(result -> { 276 var artifact = result.getArtifact(); 277 var id = new ModuleIdentifier(artifact.getGroupId(), artifact.getArtifactId()); 278 return TestResourcesClasspath.isDependencyAllowedOnServerClasspath(id); 279 }) 280 .toList(); 281 return toClasspathFiles(filteredArtifacts); 282 } 283 284 private List<MavenDependency> getApplicationDependencies() { 285 return this.mavenProject.getDependencies().stream() 286 .map(DependencyResolutionService::mavenDependencyToTestResourcesDependency) 287 .toList(); 288 } 289 290 /** 291 * Contains the logic to stop the Test Resources Service. 292 * 293 * @param quiet Whether to perform logging or not. 294 */ 295 public void stop(boolean quiet) throws MojoExecutionException { 296 if (!enabled) { 297 return; 298 } 299 if (isKeepAlive()) { 300 log("Keeping Micronaut Test Resources service alive", quiet); 301 return; 302 } 303 try { 304 Optional<ServerSettings> optionalServerSettings = ServerUtils.readServerSettings(getServerSettingsDirectory()); 305 if (optionalServerSettings.isPresent()) { 306 if (isServerStarted(optionalServerSettings.get().getPort())) { 307 log("Shutting down Micronaut Test Resources service", quiet); 308 doStop(); 309 } else { 310 log("Cannot find Micronaut Test Resources service settings, server may already be shutdown", quiet); 311 Files.deleteIfExists(getServerSettingsDirectory().resolve(PROPERTIES_FILE_NAME)); 312 } 313 if (shared && sharedServerNamespace != null) { 314 Path projectSettingsDirectory = serverSettingsDirectoryOf(buildDirectory.toPath()); 315 Files.deleteIfExists(projectSettingsDirectory.resolve(TEST_RESOURCES_PROPERTIES)); 316 } 317 } 318 } catch (Exception e) { 319 throw new MojoExecutionException("Unable to stop test resources server", e); 320 } 321 } 322 323 private static boolean isServerStarted(int port) { 324 if (System.getProperty("test.resources.internal.server.started") != null) { 325 return Boolean.getBoolean("test.resources.internal.server.started"); 326 } else { 327 return !SocketUtils.isTcpPortAvailable(port); 328 } 329 } 330 331 private void log(String message, boolean quiet) { 332 if (quiet) { 333 if (log.isDebugEnabled()) { 334 log.debug(message); 335 } 336 } else { 337 log.info(message); 338 } 339 } 340 341 private void doStop() throws IOException, MojoExecutionException { 342 try { 343 Path settingsDirectory = getServerSettingsDirectory(); 344 ServerUtils.stopServer(settingsDirectory); 345 } finally { 346 deleteKeepAliveFile(); 347 } 348 } 349 350 private void deleteKeepAliveFile() throws MojoExecutionException { 351 if (Files.exists(getKeepAliveFile())) { 352 try { 353 Files.delete(getKeepAliveFile()); 354 } catch (IOException e) { 355 throw new MojoExecutionException("Failed to delete keepalive file", e); 356 } 357 } 358 } 359 360 private Path getServerSettingsDirectory() { 361 if (shared) { 362 return ServerUtils.getDefaultSharedSettingsPath(sharedServerNamespace); 363 } 364 return serverSettingsDirectoryOf(buildDirectory.toPath()); 365 } 366 367 private Path getKeepAliveFile() { 368 var tmpDir = Path.of(System.getProperty("java.io.tmpdir")); 369 return tmpDir.resolve("keepalive-" + mavenSession.getRequest().getBuilderId()); 370 } 371 372 private Path serverSettingsDirectoryOf(Path buildDir) { 373 return buildDir.resolve("../.micronaut/test-resources"); 374 } 375 376 /** 377 * @param sharedServerNamespace The shared server namespace (if any). 378 */ 379 public void setSharedServerNamespace(String sharedServerNamespace) { 380 this.sharedServerNamespace = sharedServerNamespace; 381 } 382}