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