001/* 002 * Copyright 2017-2023 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.testresources.buildtools.ServerFactory; 019import io.micronaut.testresources.buildtools.ServerUtils; 020import org.apache.maven.execution.MavenSession; 021import org.apache.maven.plugin.logging.Log; 022import org.apache.maven.toolchain.ToolchainManager; 023 024import java.io.File; 025import java.time.Duration; 026import java.util.ArrayList; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.TimeUnit; 032import java.util.concurrent.atomic.AtomicBoolean; 033import java.util.stream.Collectors; 034 035import static io.micronaut.maven.core.MojoUtils.findJavaExecutable; 036 037/** 038 * Default implementation for {@link ServerFactory}. 039 * 040 * @author Álvaro Sánchez-Mariscal 041 * @since 4.0.0 042 */ 043public class DefaultServerFactory implements ServerFactory { 044 045 private static final Set<Process> PROCESSES = ConcurrentHashMap.newKeySet(); 046 047 private final Log log; 048 private final ToolchainManager toolchainManager; 049 private final MavenSession mavenSession; 050 private final AtomicBoolean serverStarted; 051 private final String testResourcesVersion; 052 private final boolean debugServer; 053 private final boolean foreground; 054 private final boolean stopOnShutdown; 055 private final Map<String, String> testResourcesSystemProperties; 056 057 private Process process; 058 059 public DefaultServerFactory(Log log, 060 ToolchainManager toolchainManager, 061 MavenSession mavenSession, 062 AtomicBoolean serverStarted, 063 String testResourcesVersion, 064 boolean debugServer, 065 boolean foreground, 066 boolean stopOnShutdown, 067 final Map<String, String> testResourcesSystemProperties) { 068 this.log = log; 069 this.toolchainManager = toolchainManager; 070 this.mavenSession = mavenSession; 071 this.serverStarted = serverStarted; 072 this.testResourcesVersion = testResourcesVersion; 073 this.debugServer = debugServer; 074 this.foreground = foreground; 075 this.stopOnShutdown = stopOnShutdown; 076 this.testResourcesSystemProperties = testResourcesSystemProperties; 077 } 078 079 @Override 080 public void startServer(ServerUtils.ProcessParameters processParameters) { 081 log.info("Starting Micronaut Test Resources service, version " + testResourcesVersion); 082 var cli = computeCliArguments(processParameters); 083 084 if (log.isDebugEnabled()) { 085 log.debug(String.format("Command parameters: %s", String.join(" ", cli))); 086 } 087 088 var builder = new ProcessBuilder(cli); 089 try { 090 process = builder.inheritIO().start(); 091 PROCESSES.add(process); 092 process.onExit().thenRun(() -> PROCESSES.remove(process)); 093 if (stopOnShutdown) { 094 Runtime.getRuntime().addShutdownHook(new Thread(() -> stopServer(process))); 095 } 096 if (foreground) { 097 log.info("Test Resources Service started in foreground. Press Ctrl+C to stop."); 098 process.waitFor(); 099 } 100 } catch (InterruptedException e) { 101 log.error("Failed to start server", e); 102 Thread.currentThread().interrupt(); 103 } catch (Exception e) { 104 log.error("Failed to start server", e); 105 serverStarted.set(false); 106 if (process != null) { 107 process.destroyForcibly(); 108 } 109 } finally { 110 if (process != null) { 111 if (process.isAlive()) { 112 serverStarted.set(true); 113 } else { 114 process.destroyForcibly(); 115 } 116 } 117 } 118 } 119 120 /** 121 * Computes the command-line arguments required to run the server based on the provided process parameters. 122 * 123 * @param processParameters the process parameters containing information about JVM arguments, system properties, 124 * classpath, main class, and program arguments 125 * @return a list of command-line arguments as strings 126 * @throws IllegalStateException if the Java executable cannot be found, or if the main class is not set 127 */ 128 List<String> computeCliArguments(ServerUtils.ProcessParameters processParameters) { 129 var cli = new ArrayList<String>(); 130 131 String javaBin = findJavaExecutable(toolchainManager, mavenSession); 132 if (javaBin == null) { 133 throw new IllegalStateException("Java executable not found"); 134 } 135 cli.add(javaBin); 136 cli.addAll(processParameters.getJvmArguments()); 137 if (debugServer) { 138 cli.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"); 139 } 140 processParameters.getSystemProperties().forEach((key, value) -> cli.add("-D" + key + "=" + value)); 141 if (testResourcesSystemProperties != null && !testResourcesSystemProperties.isEmpty()) { 142 testResourcesSystemProperties.forEach((key, value) -> cli.add("-D" + key + "=" + value)); 143 } 144 cli.add("-cp"); 145 cli.add(processParameters.getClasspath().stream() 146 .map(File::getAbsolutePath) 147 .collect(Collectors.joining(File.pathSeparator))); 148 String mainClass = processParameters.getMainClass(); 149 if (mainClass == null) { 150 throw new IllegalStateException("Main class is not set"); 151 } 152 cli.add(mainClass); 153 cli.addAll(processParameters.getArguments()); 154 155 return cli; 156 } 157 158 @Override 159 public void waitFor(Duration duration) throws InterruptedException { 160 if (process != null) { 161 process.waitFor(duration.toMillis(), TimeUnit.MILLISECONDS); 162 } 163 } 164 165 static void stopAllServers() { 166 PROCESSES.forEach(DefaultServerFactory::stopServer); 167 } 168 169 private static void stopServer(Process process) { 170 PROCESSES.remove(process); 171 if (process.isAlive()) { 172 process.destroy(); 173 try { 174 if (!process.waitFor(10, TimeUnit.SECONDS)) { 175 process.destroyForcibly(); 176 } 177 } catch (InterruptedException e) { 178 process.destroyForcibly(); 179 Thread.currentThread().interrupt(); 180 } 181 } 182 } 183}