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}