Refactor usage of properties in the client / daemon, fixes #188

This commit is contained in:
Guillaume Nodet
2020-11-05 15:02:24 +01:00
committed by Peter Palaga
parent 759ca73c2d
commit 4030f8b5a4
31 changed files with 932 additions and 941 deletions

View File

@@ -1,196 +0,0 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.fuse.mvnd.client;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Supplier;
import org.jboss.fuse.mvnd.common.BuildProperties;
import org.jboss.fuse.mvnd.common.Environment;
import org.jboss.fuse.mvnd.common.Environment.ValueSource;
import org.jboss.fuse.mvnd.common.Layout;
/**
* Local paths relevant for the {@link DefaultClient}.
*/
public class ClientLayout extends Layout {
private static ClientLayout ENV_INSTANCE;
private final Path localMavenRepository;
private final Path settings;
private final Path javaHome;
private final Path logbackConfigurationPath;
private final int idleTimeoutMs;
private final int keepAliveMs;
private final int maxLostKeepAlive;
private final String threads;
public static ClientLayout getEnvInstance() {
if (ENV_INSTANCE == null) {
final Path mvndPropertiesPath = Environment.findMvndPropertiesPath();
final Supplier<Properties> mvndProperties = lazyMvndProperties(mvndPropertiesPath);
final Path pwd = Paths.get(".").toAbsolutePath().normalize();
final Path mvndHome = Environment.MVND_HOME
.fromValueSource(new ValueSource(
description -> description.append("path relative to the mvnd executable"),
() -> {
Optional<String> cmd = ProcessHandle.current().info().command();
if (Environment.isNative() && cmd.isPresent()) {
final Path mvndH = Paths.get(cmd.get()).getParent().getParent();
if (mvndH != null) {
final Path mvndDaemonLib = mvndH
.resolve("mvn/lib/ext/mvnd-daemon-" + BuildProperties.getInstance().getVersion()
+ ".jar");
if (Files.exists(mvndDaemonLib)) {
return mvndH.toString();
}
}
}
return null;
}))
.orSystemProperty()
.orEnvironmentVariable()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orFail()
.asPath()
.toAbsolutePath().normalize();
final int idleTimeoutMs = Environment.DAEMON_IDLE_TIMEOUT_MS
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> Integer.toString(Environment.DEFAULT_IDLE_TIMEOUT))
.asInt();
final int keepAliveMs = Environment.DAEMON_KEEP_ALIVE_MS
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> Integer.toString(Environment.DEFAULT_KEEP_ALIVE))
.asInt();
final int maxLostKeepAlive = Environment.DAEMON_MAX_LOST_KEEP_ALIVE
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> Integer.toString(Environment.DEFAULT_MAX_LOST_KEEP_ALIVE))
.asInt();
final String threads = Environment.MVND_THREADS
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> {
final int minThreads = Environment.MVND_MIN_THREADS
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> Integer.toString(Environment.DEFAULT_MIN_THREADS))
.asInt();
return String
.valueOf(Math.max(Runtime.getRuntime().availableProcessors() - 1, minThreads));
})
.asString();
ENV_INSTANCE = new ClientLayout(
mvndPropertiesPath,
mvndHome,
pwd,
Environment.findMultiModuleProjectDirectory(pwd),
Environment.findJavaHome(mvndProperties, mvndPropertiesPath),
findLocalRepo(),
null,
Environment.findLogbackConfigurationPath(mvndProperties, mvndPropertiesPath, mvndHome),
idleTimeoutMs, keepAliveMs, maxLostKeepAlive, threads);
}
return ENV_INSTANCE;
}
public ClientLayout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory, Path javaHome,
Path localMavenRepository, Path settings, Path logbackConfigurationPath, int idleTimeoutMs, int keepAliveMs,
int maxLostKeepAlive, String threads) {
super(mvndPropertiesPath, mavenHome, userDir, multiModuleProjectDirectory);
this.localMavenRepository = localMavenRepository;
this.settings = settings;
this.javaHome = Objects.requireNonNull(javaHome, "javaHome");
this.logbackConfigurationPath = logbackConfigurationPath;
this.idleTimeoutMs = idleTimeoutMs;
this.keepAliveMs = keepAliveMs;
this.maxLostKeepAlive = maxLostKeepAlive;
this.threads = threads;
}
/**
* @param newUserDir where to change the current directory to
* @return a new {@link ClientLayout} with {@code userDir} set to the given {@code newUserDir}
*/
public ClientLayout cd(Path newUserDir) {
return new ClientLayout(mvndPropertiesPath, mavenHome, newUserDir, multiModuleProjectDirectory, javaHome,
localMavenRepository, settings, logbackConfigurationPath, idleTimeoutMs, keepAliveMs, maxLostKeepAlive,
threads);
}
/**
* @return absolute normalized path to local Maven repository or {@code null} if the server is supposed to use the
* default
*/
public Path getLocalMavenRepository() {
return localMavenRepository;
}
/**
* @return absolute normalized path to {@code settings.xml} or {@code null}
*/
public Path getSettings() {
return settings;
}
public Path javaHome() {
return javaHome;
}
public Path getLogbackConfigurationPath() {
return logbackConfigurationPath;
}
public int getIdleTimeoutMs() {
return idleTimeoutMs;
}
public int getKeepAliveMs() {
return keepAliveMs;
}
public int getMaxLostKeepAlive() {
return maxLostKeepAlive;
}
/**
* @return the number of threads (same syntax as Maven's {@code -T}/{@code --threads} option) to pass to the daemon
* unless the user passes his own `-T` or `--threads`.
*/
public String getThreads() {
return threads;
}
static Path findLocalRepo() {
return Environment.MAVEN_REPO_LOCAL.systemProperty().asPath();
}
@Override
public String toString() {
return "ClientLayout [localMavenRepository=" + localMavenRepository + ", settings=" + settings + ", javaHome="
+ javaHome + ", mavenHome()=" + mavenHome() + ", userDir()=" + userDir() + ", registry()=" + registry()
+ ", multiModuleProjectDirectory()=" + multiModuleProjectDirectory() + "]";
}
}

View File

@@ -25,12 +25,10 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.jboss.fuse.mvnd.common.DaemonConnection;
import org.jboss.fuse.mvnd.common.DaemonDiagnostics;
import org.jboss.fuse.mvnd.common.DaemonException;
import org.jboss.fuse.mvnd.common.DaemonException.ConnectException;
import org.jboss.fuse.mvnd.common.DaemonException.StaleAddressException;
import org.jboss.fuse.mvnd.common.DaemonInfo;
import org.jboss.fuse.mvnd.common.Layout;
import org.jboss.fuse.mvnd.common.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,10 +52,10 @@ public class DaemonClientConnection implements Closeable {
private final Thread receiver;
private final AtomicBoolean running = new AtomicBoolean(true);
private final AtomicReference<Exception> exception = new AtomicReference<>();
private final Layout layout;
private final DaemonParameters parameters;
public DaemonClientConnection(DaemonConnection connection, DaemonInfo daemon,
StaleAddressDetector staleAddressDetector, boolean newDaemon, int maxKeepAliveMs, Layout layout) {
StaleAddressDetector staleAddressDetector, boolean newDaemon, int maxKeepAliveMs, DaemonParameters parameters) {
this.connection = connection;
this.daemon = daemon;
this.staleAddressDetector = staleAddressDetector;
@@ -65,7 +63,7 @@ public class DaemonClientConnection implements Closeable {
this.maxKeepAliveMs = maxKeepAliveMs;
this.receiver = new Thread(this::doReceive);
this.receiver.start();
this.layout = layout;
this.parameters = parameters;
}
public DaemonInfo getDaemon() {
@@ -105,7 +103,7 @@ public class DaemonClientConnection implements Closeable {
+ "ms, daemon may have crashed. You may want to check its status using mvnd --status");
}
} catch (Exception e) {
DaemonDiagnostics diag = new DaemonDiagnostics(daemon.getUid(), layout);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon.getUid(), parameters);
LOG.debug("Problem receiving message to the daemon. Performing 'on failure' operation...");
if (!hasReceived && newDaemon) {
throw new ConnectException("Could not receive a message from the daemon.\n" + diag.describe(), e);

View File

@@ -34,7 +34,6 @@ import org.jboss.fuse.mvnd.common.BuildProperties;
import org.jboss.fuse.mvnd.common.DaemonCompatibilitySpec;
import org.jboss.fuse.mvnd.common.DaemonCompatibilitySpec.Result;
import org.jboss.fuse.mvnd.common.DaemonConnection;
import org.jboss.fuse.mvnd.common.DaemonDiagnostics;
import org.jboss.fuse.mvnd.common.DaemonException;
import org.jboss.fuse.mvnd.common.DaemonInfo;
import org.jboss.fuse.mvnd.common.DaemonRegistry;
@@ -63,13 +62,11 @@ public class DaemonConnector {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonConnector.class);
private final DaemonRegistry registry;
private final ClientLayout layout;
private final BuildProperties buildProperties;
private final DaemonParameters parameters;
public DaemonConnector(ClientLayout layout, DaemonRegistry registry, BuildProperties buildProperties) {
this.layout = layout;
public DaemonConnector(DaemonParameters parameters, DaemonRegistry registry) {
this.parameters = parameters;
this.registry = registry;
this.buildProperties = buildProperties;
}
public DaemonClientConnection maybeConnect(DaemonCompatibilitySpec constraint) {
@@ -85,7 +82,9 @@ public class DaemonConnector {
return null;
}
public DaemonClientConnection connect(DaemonCompatibilitySpec constraint, ClientOutput output) {
public DaemonClientConnection connect(ClientOutput output) {
final DaemonCompatibilitySpec constraint = new DaemonCompatibilitySpec(
parameters.javaHome(), parameters.getDaemonOpts());
output.buildStatus("Looking up daemon...");
Map<Boolean, List<DaemonInfo>> idleBusy = registry.getAll().stream()
.collect(Collectors.groupingBy(di -> di.getState() == DaemonState.Idle));
@@ -107,7 +106,7 @@ public class DaemonConnector {
// No compatible daemons available - start a new daemon
String message = handleStopEvents(idleDaemons, busyDaemons);
output.buildStatus(message);
return startDaemon(constraint);
return startDaemon();
}
private String handleStopEvents(Collection<DaemonInfo> idleDaemons, Collection<DaemonInfo> busyDaemons) {
@@ -221,9 +220,9 @@ public class DaemonConnector {
return null;
}
public DaemonClientConnection startDaemon(DaemonCompatibilitySpec constraint) {
public DaemonClientConnection startDaemon() {
final String daemon = UUID.randomUUID().toString();
final Process process = startDaemon(daemon, constraint.getOptions());
final Process process = startDaemon(daemon);
LOGGER.debug("Started Maven daemon {}", daemon);
long start = System.currentTimeMillis();
do {
@@ -237,41 +236,49 @@ public class DaemonConnector {
throw new DaemonException.InterruptedException(e);
}
} while (process.isAlive() && System.currentTimeMillis() - start < DEFAULT_CONNECT_TIMEOUT);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, parameters);
throw new DaemonException.ConnectException("Timeout waiting to connect to the Maven daemon.\n" + diag.describe());
}
private Process startDaemon(String uid, List<String> opts) {
final Path mavenHome = layout.mavenHome();
final Path workingDir = layout.userDir();
private Process startDaemon(String uid) {
final Path mvndHome = parameters.mvndHome();
final Path workingDir = parameters.userDir();
String command = "";
try {
final String classpath = mavenHome.resolve("mvn/lib/ext/mvnd-common-" + buildProperties.getVersion() + ".jar")
.toString();
final String java = Os.current().isUnixLike() ? "bin/java" : "bin\\java.exe";
List<String> args = new ArrayList<>();
args.add(layout.javaHome().resolve(java).toString());
// executable
final String java = Os.current().isUnixLike() ? "bin/java" : "bin\\java.exe";
args.add(parameters.javaHome().resolve(java).toString());
// classpath
args.add("-classpath");
final String mvndCommonPath = "mvn/lib/ext/mvnd-common-" + BuildProperties.getInstance().getVersion() + ".jar";
final String classpath = mvndHome.resolve(mvndCommonPath).toString();
args.add(classpath);
if (Environment.DAEMON_DEBUG.systemProperty().orDefault(() -> "false").asBoolean()) {
// debug options
if (parameters.property(Environment.DAEMON_DEBUG).asBoolean()) {
args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000");
}
args.add("-Dmvnd.home=" + mavenHome);
args.add("-Dmvnd.java.home=" + layout.javaHome().toString());
args.add("-Dlogback.configurationFile=" + layout.getLogbackConfigurationPath());
args.add("-Ddaemon.uid=" + uid);
if (Boolean.getBoolean(Environment.DEBUG_ENVIRONMENT_PROP)) {
args.add("-D" + Environment.DEBUG_ENVIRONMENT_PROP + "=true");
// memory
String minHeapSize = parameters.minHeapSize();
if (minHeapSize != null) {
args.add("-Xms" + minHeapSize);
}
args.add("-Xmx4g");
args.add(Environment.DAEMON_IDLE_TIMEOUT_MS.asCommandLineProperty(Integer.toString(layout.getIdleTimeoutMs())));
args.add(Environment.DAEMON_KEEP_ALIVE_MS.asCommandLineProperty(Integer.toString(layout.getKeepAliveMs())));
args.addAll(opts);
String maxHeapSize = parameters.maxHeapSize();
if (maxHeapSize != null) {
args.add("-Xmx" + maxHeapSize);
}
args.add(Environment.MVND_HOME.asCommandLineProperty(mvndHome.toString()));
args.add(Environment.LOGBACK_CONFIGURATION_FILE
.asCommandLineProperty(parameters.logbackConfigurationPath().toString()));
args.add(Environment.DAEMON_UID.asCommandLineProperty(uid));
args.add(Environment.DAEMON_REGISTRY.asCommandLineProperty(parameters.registry().toString()));
args.addAll(parameters.getDaemonCommandLineProperties());
args.add(MavenDaemon.class.getName());
command = String.join(" ", args);
LOGGER.debug("Starting daemon process: uid = {}, workingDir = {}, daemonArgs: {}", uid, workingDir, command);
ProcessBuilder.Redirect redirect = ProcessBuilder.Redirect.appendTo(layout.daemonOutLog(uid).toFile());
ProcessBuilder.Redirect redirect = ProcessBuilder.Redirect.appendTo(parameters.daemonOutLog(uid).toFile());
Process process = new ProcessBuilder()
.directory(workingDir.toFile())
.command(args)
@@ -296,7 +303,7 @@ public class DaemonConnector {
try {
return connectToDaemon(daemonInfo, new CleanupOnStaleAddress(daemonInfo), newDaemon);
} catch (DaemonException.ConnectException e) {
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout);
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, parameters);
throw new DaemonException.ConnectException("Could not connect to the Maven daemon.\n" + diag.describe(), e);
}
}
@@ -308,9 +315,9 @@ public class DaemonConnector {
throws DaemonException.ConnectException {
LOGGER.debug("Connecting to Daemon");
try {
int maxKeepAliveMs = layout.getKeepAliveMs() * layout.getMaxLostKeepAlive();
int maxKeepAliveMs = parameters.keepAliveMs() * parameters.maxLostKeepAlive();
DaemonConnection connection = connect(daemon.getAddress());
return new DaemonClientConnection(connection, daemon, staleAddressDetector, newDaemon, maxKeepAliveMs, layout);
return new DaemonClientConnection(connection, daemon, staleAddressDetector, newDaemon, maxKeepAliveMs, parameters);
} catch (DaemonException.ConnectException e) {
staleAddressDetector.maybeStaleAddress(e);
throw e;

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.fuse.mvnd.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.stream.Collector;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/diagnostics/DaemonDiagnostics.java
*/
public class DaemonDiagnostics {
private final static int TAIL_SIZE = 20;
private final String uid;
private final DaemonParameters parameters;
public DaemonDiagnostics(String uid, DaemonParameters parameters) {
this.uid = uid;
this.parameters = parameters;
}
@Override
public String toString() {
return "{"
+ "uid=" + uid
+ ", parameters=" + parameters
+ '}';
}
public String describe() {
StringBuilder sb = new StringBuilder();
sb.append("Daemon uid: ").append(uid).append("\n");
tail(sb, "log file", parameters.daemonLog(uid));
tail(sb, "output", parameters.daemonOutLog(uid));
return sb.toString();
}
static void tail(StringBuilder sb, String name, Path log) {
try {
String tail = tail(log);
sb.append(" ").append(name).append(": ").append(log).append("\n");
sb.append("----- Last " + TAIL_SIZE + " lines from daemon ").append(name).append(" - ").append(log)
.append(" -----\n");
sb.append(tail);
sb.append("----- End of the daemon ").append(name).append(" -----\n");
} catch (NoSuchFileException e) {
sb.append(" no ").append(name).append(" at: ").append(log).append("\n");
} catch (IOException e) {
sb.append(" unable to read from the daemon ").append(name).append(": ").append(log).append(", because of: ")
.append(e);
}
}
/**
* @param path to read from tail
* @return tail content
* @throws IOException when reading failed
*/
static String tail(Path path) throws IOException {
try (BufferedReader r = Files.newBufferedReader(path)) {
return String.join("\n", r.lines().collect(lastN(TAIL_SIZE))) + "\n";
}
}
static <T> Collector<T, ?, List<T>> lastN(int n) {
return Collector.<T, Deque<T>, List<T>> of(ArrayDeque::new, (acc, t) -> {
if (acc.size() == n)
acc.pollFirst();
acc.add(t);
}, (acc1, acc2) -> {
while (acc2.size() < n && !acc1.isEmpty()) {
acc2.addFirst(acc1.pollLast());
}
return acc2;
}, ArrayList::new);
}
}

View File

@@ -0,0 +1,522 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.fuse.mvnd.client;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.maven.cli.internal.extension.model.CoreExtension;
import org.apache.maven.cli.internal.extension.model.io.xpp3.CoreExtensionsXpp3Reader;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.jboss.fuse.mvnd.common.BuildProperties;
import org.jboss.fuse.mvnd.common.Environment;
import org.jboss.fuse.mvnd.common.Os;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Hold all daemon configuration
*/
public class DaemonParameters {
private static final Logger LOG = LoggerFactory.getLogger(DaemonParameters.class);
private static final String EXT_CLASS_PATH = "maven.ext.class.path";
private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml";
protected final Map<Path, Properties> mvndProperties = new ConcurrentHashMap<>();
protected final Function<Path, Properties> provider = path -> mvndProperties.computeIfAbsent(path,
p -> loadProperties(path));
protected final Properties properties;
public DaemonParameters(Properties properties) {
this.properties = properties;
}
public List<String> getDaemonOpts() {
return Arrays.stream(Environment.values())
.filter(Environment::isDiscriminating)
.map(v -> v.asDaemonOpt(property(v).orFail().asString()))
.collect(Collectors.toList());
}
public List<String> getDaemonCommandLineProperties() {
return Arrays.stream(Environment.values())
.filter(Environment::isDiscriminating)
.map(v -> v.asCommandLineProperty(property(v).orFail().asString()))
.collect(Collectors.toList());
}
public Path mvndHome() {
return value(Environment.MVND_HOME)
.or(new ValueSource(
description -> description.append("path relative to the mvnd executable"),
this::mvndHomeFromExecutable))
.orSystemProperty()
.orEnvironmentVariable()
.orLocalProperty(provider, suppliedPropertiesPath())
.orLocalProperty(provider, localPropertiesPath())
.orLocalProperty(provider, userPropertiesPath())
.orFail()
.asPath()
.toAbsolutePath().normalize();
}
private String mvndHomeFromExecutable() {
Optional<String> cmd = ProcessHandle.current().info().command();
if (Environment.isNative() && cmd.isPresent()) {
final Path mvndH = Paths.get(cmd.get()).getParent().getParent();
if (mvndH != null) {
final Path mvndDaemonLib = mvndH
.resolve("mvn/lib/ext/mvnd-daemon-" + BuildProperties.getInstance().getVersion() + ".jar");
if (Files.exists(mvndDaemonLib)) {
return mvndH.toString();
}
}
}
return null;
}
public Path javaHome() {
final Path result = value(Environment.JAVA_HOME)
.orEnvironmentVariable()
.orLocalProperty(provider, suppliedPropertiesPath())
.orLocalProperty(provider, localPropertiesPath())
.orLocalProperty(provider, userPropertiesPath())
.orLocalProperty(provider, globalPropertiesPath())
.orSystemProperty()
.orFail()
.asPath();
try {
return result.toRealPath();
} catch (IOException e) {
throw new RuntimeException("Could not get a real path from path " + result);
}
}
public Path userDir() {
return value(Environment.USER_DIR)
.orSystemProperty()
.orFail()
.asPath();
}
public Path userHome() {
return value(Environment.USER_HOME)
.orSystemProperty()
.orFail()
.asPath();
}
public Path suppliedPropertiesPath() {
return value(Environment.MVND_PROPERTIES_PATH)
.orEnvironmentVariable()
.orSystemProperty()
.asPath();
}
public Path localPropertiesPath() {
return multiModuleProjectDirectory().resolve(".mvn/mvnd.properties");
}
public Path userPropertiesPath() {
return userHome().resolve(".m2/mvnd.properties");
}
public Path globalPropertiesPath() {
return mvndHome().resolve("conf/mvnd.properties");
}
public Path daemonStorage() {
return value(Environment.MVND_DAEMON_STORAGE)
.orSystemProperty()
.orDefault(() -> userHome().resolve(".mvnd/v" + BuildProperties.getInstance().getVersion()).toString())
.asPath();
}
public Path registry() {
return daemonStorage().resolve("registry.bin");
}
public Path daemonLog(String daemon) {
return daemonStorage().resolve("daemon-" + daemon + ".log");
}
public Path daemonOutLog(String daemon) {
return daemonStorage().resolve("daemon-" + daemon + ".out.log");
}
public Path multiModuleProjectDirectory() {
return value(Environment.MAVEN_MULTIMODULE_PROJECT_DIRECTORY)
.orSystemProperty()
.orDefault(() -> findDefaultMultimoduleProjectDirectory(userDir()))
.asPath()
.toAbsolutePath().normalize();
}
public Path logbackConfigurationPath() {
return property(Environment.LOGBACK_CONFIGURATION_FILE)
.orDefault(() -> mvndHome().resolve("mvn/conf/logging/logback.xml").toString())
.orFail()
.asPath();
}
public String minHeapSize() {
return property(Environment.DAEMON_MIN_HEAP_SIZE).asString();
}
public String maxHeapSize() {
return property(Environment.DAEMON_MAX_HEAP_SIZE).asString();
}
/**
* @return the number of threads (same syntax as Maven's {@code -T}/{@code --threads} option) to pass to the daemon
* unless the user passes his own `-T` or `--threads`.
*/
public String threads() {
return property(Environment.MVND_THREADS)
.orDefault(() -> String.valueOf(property(Environment.MVND_MIN_THREADS)
.asInt(m -> Math.max(Runtime.getRuntime().availableProcessors() - 1, m))))
.orFail()
.asString();
}
public String builder() {
return property(Environment.MVND_BUILDER).orFail().asString();
}
/**
* @return absolute normalized path to {@code settings.xml} or {@code null}
*/
public Path settings() {
return property(Environment.MAVEN_SETTINGS).asPath();
}
/**
* @return absolute normalized path to local Maven repository or {@code null} if the server is supposed to use the
* default
*/
public Path mavenRepoLocal() {
return property(Environment.MAVEN_REPO_LOCAL).asPath();
}
/**
* @param newUserDir where to change the current directory to
* @return a new {@link DaemonParameters} with {@code userDir} set to the given {@code newUserDir}
*/
public DaemonParameters cd(Path newUserDir) {
Properties properties = new Properties();
properties.putAll(this.properties);
properties.put(Environment.USER_DIR.getProperty(), newUserDir.toString());
return new DaemonParameters(properties);
}
public int keepAliveMs() {
return property(Environment.DAEMON_KEEP_ALIVE_MS).orFail().asInt();
}
public int maxLostKeepAlive() {
return property(Environment.DAEMON_MAX_LOST_KEEP_ALIVE).orFail().asInt();
}
public static String findDefaultMultimoduleProjectDirectory(Path pwd) {
Path dir = pwd;
do {
if (Files.isDirectory(dir.resolve(".mvn"))) {
return dir.toString();
}
dir = dir.getParent();
} while (dir != null);
/*
* Return pwd if .mvn directory was not found in the hierarchy.
* Maven does the same thing in mvn shell script's find_maven_basedir()
* and find_file_argument_basedir() routines
*/
return pwd.toString();
}
public EnvValue property(Environment env) {
return value(env)
.orSystemProperty()
.orLocalProperty(provider, suppliedPropertiesPath())
.orLocalProperty(provider, localPropertiesPath())
.orLocalProperty(provider, userPropertiesPath())
.orLocalProperty(provider, globalPropertiesPath())
.orDefault(() -> defaultValue(env));
}
protected EnvValue value(Environment env) {
return new EnvValue(env, new ValueSource(
description -> description.append("value: ").append(env.getProperty()),
() -> properties.getProperty(env.getProperty())));
}
public static EnvValue systemProperty(Environment env) {
return new EnvValue(env, EnvValue.systemPropertySource(env));
}
public static EnvValue environmentVariable(Environment env) {
return new EnvValue(env, EnvValue.environmentVariableSource(env));
}
public static EnvValue fromValueSource(Environment env, ValueSource valueSource) {
return new EnvValue(env, valueSource);
}
private String defaultValue(Environment env) {
if (env == Environment.DAEMON_EXT_CLASSPATH) {
List<String> cp = parseExtClasspath(userHome());
return String.join(",", cp);
} else if (env == Environment.DAEMON_CORE_EXTENSIONS) {
try {
List<String> extensions = readCoreExtensionsDescriptor(multiModuleProjectDirectory()).stream()
.map(e -> e.getGroupId() + ":" + e.getArtifactId() + ":" + e.getVersion())
.collect(Collectors.toList());
return String.join(",", extensions);
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Unable to parse core extensions", e);
}
} else {
return env.getDef();
}
}
private static List<String> parseExtClasspath(Path userDir) {
String extClassPath = System.getProperty(EXT_CLASS_PATH);
List<String> jars = new ArrayList<>();
if (StringUtils.isNotEmpty(extClassPath)) {
for (String jar : StringUtils.split(extClassPath, File.pathSeparator)) {
Path path = userDir.resolve(jar).toAbsolutePath();
jars.add(path.toString());
}
}
return jars;
}
private static List<CoreExtension> readCoreExtensionsDescriptor(Path multiModuleProjectDirectory)
throws IOException, XmlPullParserException {
if (multiModuleProjectDirectory == null) {
return Collections.emptyList();
}
Path extensionsFile = multiModuleProjectDirectory.resolve(EXTENSIONS_FILENAME);
if (!Files.exists(extensionsFile)) {
return Collections.emptyList();
}
CoreExtensionsXpp3Reader parser = new CoreExtensionsXpp3Reader();
try (InputStream is = Files.newInputStream(extensionsFile)) {
return parser.read(is).getExtensions();
}
}
private static Properties loadProperties(Path path) {
Properties result = new Properties();
if (Files.exists(path)) {
try (InputStream in = Files.newInputStream(path)) {
result.load(in);
} catch (IOException e) {
throw new RuntimeException("Could not read " + path);
}
}
return result;
}
/**
* A source of an environment value with a description capability.
*/
public static class ValueSource {
final Function<StringBuilder, StringBuilder> descriptionFunction;
final Supplier<String> valueSupplier;
public ValueSource(Function<StringBuilder, StringBuilder> descriptionFunction, Supplier<String> valueSupplier) {
this.descriptionFunction = descriptionFunction;
this.valueSupplier = valueSupplier;
}
/** Mostly for debugging */
@Override
public String toString() {
return descriptionFunction.apply(new StringBuilder()).toString();
}
}
/**
* A chained lazy environment value.
*/
public static class EnvValue {
static Map<String, String> env = System.getenv();
private final Environment envKey;
private final ValueSource valueSource;
protected EnvValue previous;
public EnvValue(Environment envKey, ValueSource valueSource) {
this.previous = null;
this.envKey = envKey;
this.valueSource = valueSource;
}
public EnvValue(EnvValue previous, Environment envKey, ValueSource valueSource) {
this.previous = previous;
this.envKey = envKey;
this.valueSource = valueSource;
}
private static ValueSource systemPropertySource(Environment env) {
String property = env.getProperty();
if (property == null) {
throw new IllegalStateException("Cannot use " + Environment.class.getName() + " for getting a system property");
}
return new ValueSource(
description -> description.append("system property ").append(property),
() -> Environment.getProperty(property));
}
private static ValueSource environmentVariableSource(Environment env) {
String envVar = env.getEnvironmentVariable();
if (envVar == null) {
throw new IllegalStateException(
"Cannot use " + Environment.class.getName() + "." + env.name()
+ " for getting an environment variable");
}
return new ValueSource(
description -> description.append("environment variable ").append(envVar),
() -> EnvValue.env.get(envVar));
}
public EnvValue orSystemProperty() {
return new EnvValue(this, envKey, systemPropertySource(envKey));
}
public EnvValue orLocalProperty(Function<Path, Properties> provider, Path localPropertiesPath) {
if (localPropertiesPath != null) {
return new EnvValue(this, envKey, new ValueSource(
description -> description.append("property ").append(envKey.getProperty()).append(" in ")
.append(localPropertiesPath),
() -> provider.apply(localPropertiesPath).getProperty(envKey.getProperty())));
} else {
return this;
}
}
public EnvValue orEnvironmentVariable() {
return new EnvValue(this, envKey, environmentVariableSource(envKey));
}
public EnvValue or(ValueSource source) {
return new EnvValue(this, envKey, source);
}
public EnvValue orDefault() {
return orDefault(envKey::getDef);
}
public EnvValue orDefault(Supplier<String> defaultSupplier) {
return new EnvValue(this, envKey,
new ValueSource(sb -> sb.append("default: ").append(defaultSupplier.get()), defaultSupplier));
}
public EnvValue orFail() {
return new EnvValue(this, envKey, new ValueSource(sb -> sb, () -> {
final StringBuilder sb = new StringBuilder("Could not get value for ")
.append(Environment.class.getSimpleName())
.append(".").append(envKey.name()).append(" from any of the following sources: ");
/*
* Compose the description functions to invert the order thus getting the resolution order in the
* message
*/
Function<StringBuilder, StringBuilder> description = (s -> s);
EnvValue val = this;
while (val != null) {
description = description.compose(val.valueSource.descriptionFunction);
val = val.previous;
if (val != null) {
description = description.compose(s -> s.append(", "));
}
}
description.apply(sb);
throw new IllegalStateException(sb.toString());
}));
}
String get() {
if (previous != null) {
final String result = previous.get();
if (result != null) {
return result;
}
}
final String result = valueSource.valueSupplier.get();
if (result != null && LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder("Loaded environment value for key [")
.append(envKey.name())
.append("] from ");
valueSource.descriptionFunction.apply(sb);
sb.append(": [")
.append(result)
.append(']');
LOG.debug(sb.toString());
}
return result;
}
public String asString() {
return get();
}
public Optional<String> asOptional() {
return Optional.ofNullable(get());
}
public Path asPath() {
String result = get();
if (result != null && Os.current().isCygwin()) {
result = Environment.cygpath(result);
}
return result == null ? null : Paths.get(result);
}
public boolean asBoolean() {
return Boolean.parseBoolean(get());
}
public int asInt() {
return Integer.parseInt(get());
}
public int asInt(IntUnaryOperator function) {
return function.applyAsInt(asInt());
}
}
}

View File

@@ -15,27 +15,17 @@
*/
package org.jboss.fuse.mvnd.client;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.maven.cli.internal.extension.model.CoreExtension;
import org.apache.maven.cli.internal.extension.model.io.xpp3.CoreExtensionsXpp3Reader;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.fusesource.jansi.Ansi;
import org.jboss.fuse.mvnd.common.BuildProperties;
import org.jboss.fuse.mvnd.common.DaemonCompatibilitySpec;
import org.jboss.fuse.mvnd.common.DaemonInfo;
import org.jboss.fuse.mvnd.common.DaemonRegistry;
import org.jboss.fuse.mvnd.common.Environment;
@@ -54,15 +44,11 @@ import org.slf4j.LoggerFactory;
public class DefaultClient implements Client {
public static final int DEFAULT_PERIODIC_CHECK_INTERVAL_MILLIS = 10 * 1000;
public static final int CANCEL_TIMEOUT = 10 * 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class);
private static final String EXT_CLASS_PATH = "maven.ext.class.path";
private static final String EXTENSIONS_FILENAME = ".mvn/extensions.xml";
private final Supplier<ClientLayout> lazyLayout;
private final BuildProperties buildProperties;
private final Supplier<DaemonParameters> lazyParameters;
public static void main(String[] argv) throws Exception {
final List<String> args = new ArrayList<>(argv.length);
@@ -83,13 +69,12 @@ public class DefaultClient implements Client {
}
try (TerminalOutput output = new TerminalOutput(logFile)) {
new DefaultClient(ClientLayout::getEnvInstance, BuildProperties.getInstance()).execute(output, args);
new DefaultClient(() -> new DaemonParameters(new Properties())).execute(output, args);
}
}
public DefaultClient(Supplier<ClientLayout> layout, BuildProperties buildProperties) {
this.lazyLayout = layout;
this.buildProperties = buildProperties;
public DefaultClient(Supplier<DaemonParameters> lazyParameters) {
this.lazyParameters = lazyParameters;
}
@Override
@@ -135,6 +120,7 @@ public class DefaultClient implements Client {
// Print version if needed
if (version || showVersion || debug) {
// Print mvnd version
BuildProperties buildProperties = BuildProperties.getInstance();
final String nativeSuffix = Environment.isNative() ? " (native)" : "";
final String v = Ansi.ansi().bold().a(
"Maven Daemon "
@@ -158,9 +144,8 @@ public class DefaultClient implements Client {
*/
}
final ClientLayout layout = lazyLayout.get();
final Path javaHome = layout.javaHome();
try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) {
final DaemonParameters parameters = lazyParameters.get();
try (DaemonRegistry registry = new DaemonRegistry(parameters.registry())) {
boolean status = args.remove("--status");
if (status) {
final String template = " %36s %7s %5s %7s %5s %23s %s";
@@ -200,27 +185,32 @@ public class DefaultClient implements Client {
return new DefaultResult(argv, null);
}
setDefaultArgs(args, layout);
final Path settings = layout.getSettings();
if (args.stream().noneMatch(arg -> arg.startsWith("-T") || arg.equals("--threads"))) {
args.add("--threads");
args.add(parameters.threads());
}
if (args.stream().noneMatch(arg -> arg.startsWith("-b") || arg.equals("--builder"))) {
args.add("--builder");
args.add(parameters.builder());
}
final Path settings = parameters.settings();
if (settings != null && args.stream().noneMatch(arg -> arg.equals("-s") || arg.equals("--settings"))) {
args.add("-s");
args.add("--settings");
args.add(settings.toString());
}
final Path localMavenRepository = layout.getLocalMavenRepository();
if (localMavenRepository != null) {
final Path localMavenRepository = parameters.mavenRepoLocal();
if (localMavenRepository != null && args.stream().noneMatch(arg -> arg.startsWith("-Dmaven.repo.local="))) {
args.add("-Dmaven.repo.local=" + localMavenRepository.toString());
}
List<String> opts = getDaemonOpts(layout);
final DaemonConnector connector = new DaemonConnector(layout, registry, buildProperties);
try (DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome, opts), output)) {
final DaemonConnector connector = new DaemonConnector(parameters, registry);
try (DaemonClientConnection daemon = connector.connect(output)) {
output.buildStatus("Connected to daemon");
daemon.dispatch(new Message.BuildRequest(
args,
layout.userDir().toString(),
layout.multiModuleProjectDirectory().toString(),
parameters.userDir().toString(),
parameters.multiModuleProjectDirectory().toString(),
System.getenv()));
output.buildStatus("Build request sent");
@@ -259,65 +249,6 @@ public class DefaultClient implements Client {
}
}
private List<String> getDaemonOpts(ClientLayout layout) {
List<String> options = new ArrayList<>();
// Classpath
List<Path> jars = parseExtClasspath(layout);
if (!jars.isEmpty()) {
options.add(Environment.DAEMON_EXT_CLASSPATH.asCommandLineProperty(
jars.stream().map(Path::toString).collect(Collectors.joining(","))));
}
// Extensions
try {
List<CoreExtension> extensions = readCoreExtensionsDescriptor(layout);
if (!extensions.isEmpty()) {
options.add(Environment.DAEMON_CORE_EXTENSIONS.asCommandLineProperty(
extensions.stream().map(e -> e.getGroupId() + ":" + e.getArtifactId() + ":" + e.getVersion())
.collect(Collectors.joining(","))));
}
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Unable to parse core extensions", e);
}
return options;
}
private List<Path> parseExtClasspath(ClientLayout layout) {
String extClassPath = System.getProperty(EXT_CLASS_PATH);
List<Path> jars = new ArrayList<>();
if (StringUtils.isNotEmpty(extClassPath)) {
for (String jar : StringUtils.split(extClassPath, File.pathSeparator)) {
Path path = layout.userDir().resolve(jar).toAbsolutePath();
jars.add(path);
}
}
return jars;
}
private List<CoreExtension> readCoreExtensionsDescriptor(ClientLayout layout)
throws IOException, XmlPullParserException {
Path multiModuleProjectDirectory = layout.multiModuleProjectDirectory();
if (multiModuleProjectDirectory == null) {
return Collections.emptyList();
}
Path extensionsFile = multiModuleProjectDirectory.resolve(EXTENSIONS_FILENAME);
if (!Files.exists(extensionsFile)) {
return Collections.emptyList();
}
CoreExtensionsXpp3Reader parser = new CoreExtensionsXpp3Reader();
try (InputStream is = Files.newInputStream(extensionsFile)) {
return parser.read(is).getExtensions();
}
}
static void setDefaultArgs(List<String> args, ClientLayout layout) {
if (args.stream().noneMatch(arg -> arg.startsWith("-T") || arg.equals("--threads"))) {
args.add("-T" + layout.getThreads());
}
if (args.stream().noneMatch(arg -> arg.startsWith("-b") || arg.equals("--builder"))) {
args.add("-bsmart");
}
}
private static class DefaultResult implements ExecutionResult {
private final Exception exception;

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.fuse.mvnd.client;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.jboss.fuse.mvnd.common.Environment;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class EnvironmentTest {
@Test
void prop() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.props("mvnd.home", "/maven/home/prop");
Assertions.assertEquals("/maven/home/prop", DaemonParameters.systemProperty(Environment.MVND_HOME).asString());
}
}
@Test
void env() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.env("MVND_HOME", "/maven/home/env");
Assertions.assertEquals("/maven/home/env", DaemonParameters.environmentVariable(Environment.MVND_HOME).asString());
}
}
@Test
void localProps() {
try (EnvironmentResource env = new EnvironmentResource()) {
final Properties localProps = new Properties();
localProps.put("mvnd.home", "/maven/home/local");
Assertions.assertEquals(Paths.get("/maven/home/local"),
DaemonParameters
.environmentVariable(Environment.MVND_HOME)
.orSystemProperty()
.orLocalProperty(path -> localProps, Paths.get("/local/properties"))
.orFail()
.asPath());
}
}
@Test
void envBeforeProp() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.props("mvnd.home", "/maven/home/prop");
env.env("MVND_HOME", "/maven/home/env");
Assertions.assertEquals("/maven/home/env",
DaemonParameters
.environmentVariable(Environment.MVND_HOME)
.orSystemProperty()
.asString());
}
}
@Test
void fail() {
try (EnvironmentResource env = new EnvironmentResource()) {
try {
Assertions.assertEquals("/maven/home/env",
DaemonParameters
.environmentVariable(Environment.MVND_HOME)
.orSystemProperty()
.orFail()
.asString());
Assertions.fail("IllegalStateException expected");
} catch (IllegalStateException e) {
Assertions.assertEquals(
"Could not get value for Environment.MVND_HOME from any of the following sources: environment variable MVND_HOME, system property mvnd.home",
e.getMessage());
}
}
}
@Test
void cygwin() {
Assertions.assertEquals("C:\\jdk-11.0.2\\", Environment.cygpath("/cygdrive/c/jdk-11.0.2/"));
}
static class EnvironmentResource implements AutoCloseable {
private final Properties props = new Properties();
private final Map<String, String> env = new HashMap<>();
public EnvironmentResource() {
DaemonParameters.EnvValue.env = env;
Environment.setProperties(props);
}
public void props(String... props) {
int i = 0;
while (i < props.length) {
this.props.setProperty(props[i++], props[i++]);
}
}
public void env(String... env) {
int i = 0;
while (i < env.length) {
this.env.put(env[i++], env[i++]);
}
}
@Override
public void close() {
DaemonParameters.EnvValue.env = System.getenv();
Environment.setProperties(System.getProperties());
}
}
}