| /* |
| * 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.mvndaemon.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.Duration; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| 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.Consumer; |
| import java.util.function.Function; |
| import java.util.function.IntUnaryOperator; |
| import java.util.function.Supplier; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| 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.mvndaemon.mvnd.common.Environment; |
| import org.mvndaemon.mvnd.common.InterpolationHelper; |
| import org.mvndaemon.mvnd.common.Os; |
| import org.mvndaemon.mvnd.common.SocketFamily; |
| import org.mvndaemon.mvnd.common.TimeUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Hold all daemon configuration |
| */ |
| public class DaemonParameters { |
| |
| public static final String LOG_EXTENSION = ".log"; |
| |
| 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"; |
| private static final String ENV_PREFIX = "env."; |
| |
| protected final Map<Path, Properties> mvndProperties = new ConcurrentHashMap<>(); |
| protected final Function<Path, Properties> provider = path -> mvndProperties.computeIfAbsent(path, |
| p -> loadProperties(path)); |
| private final Map<String, String> properties; |
| |
| public DaemonParameters() { |
| this.properties = Collections.emptyMap(); |
| } |
| |
| protected DaemonParameters(PropertiesBuilder propertiesBuilder) { |
| this.properties = propertiesBuilder.build(); |
| } |
| |
| public List<String> getDaemonOpts() { |
| return discriminatingValues() |
| .map(envValue -> envValue.envKey.asDaemonOpt(envValue.asString())) |
| .collect(Collectors.toList()); |
| } |
| |
| public Map<String, String> getDaemonOptsMap() { |
| return discriminatingValues() |
| .collect(Collectors.toMap( |
| envValue -> envValue.envKey.getProperty(), |
| EnvValue::asString)); |
| } |
| |
| Stream<EnvValue> discriminatingValues() { |
| return Arrays.stream(Environment.values()) |
| .filter(Environment::isDiscriminating) |
| .map(this::property) |
| .filter(EnvValue::isSet); |
| } |
| |
| public void discriminatingCommandLineOptions(List<String> args) { |
| discriminatingValues() |
| .forEach(envValue -> envValue.envKey.addCommandLineOption(args, envValue.asString())); |
| } |
| |
| public Path mvndHome() { |
| return value(Environment.MVND_HOME) |
| .or(new ValueSource( |
| description -> description.append("path relative to the mvnd executable"), |
| this::mvndHomeFromExecutable)) |
| .orSystemProperty() |
| .orLocalProperty(provider, suppliedPropertiesPath()) |
| .orLocalProperty(provider, localPropertiesPath()) |
| .orLocalProperty(provider, userPropertiesPath()) |
| .orEnvironmentVariable() |
| .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) |
| .orLocalProperty(provider, suppliedPropertiesPath()) |
| .orLocalProperty(provider, localPropertiesPath()) |
| .orLocalProperty(provider, userPropertiesPath()) |
| .orLocalProperty(provider, globalPropertiesPath()) |
| .orSystemProperty() |
| .orEnvironmentVariable() |
| .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() |
| .toAbsolutePath(); |
| } |
| |
| public Path userHome() { |
| return value(Environment.USER_HOME) |
| .orSystemProperty() |
| .orFail() |
| .asPath() |
| .toAbsolutePath(); |
| } |
| |
| public Path suppliedPropertiesPath() { |
| return value(Environment.MVND_PROPERTIES_PATH) |
| .orSystemProperty() |
| .orEnvironmentVariable() |
| .asPath(); |
| } |
| |
| /** |
| * The content of the <code>.mvn/jvm.config</code> file will be read |
| * and used as arguments when starting a daemon JVM. |
| * See {@link Environment#MVND_JVM_ARGS}. |
| */ |
| public Path jvmConfigPath() { |
| return multiModuleProjectDirectory().resolve(".mvn/jvm.config"); |
| } |
| |
| 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() |
| .orLocalProperty(provider, globalPropertiesPath()) |
| .orEnvironmentVariable() |
| .orDefault( |
| () -> userHome().resolve(".m2/mvnd/registry/" + BuildProperties.getInstance().getVersion()).toString()) |
| .asPath(); |
| } |
| |
| public Path registry() { |
| return daemonStorage().resolve("registry.bin"); |
| } |
| |
| public Path daemonLog(String daemon) { |
| return daemonStorage().resolve("daemon-" + daemon + LOG_EXTENSION); |
| } |
| |
| public Path daemonOutLog(String daemon) { |
| return daemonStorage().resolve("daemon-" + daemon + ".out" + LOG_EXTENSION); |
| } |
| |
| public Path multiModuleProjectDirectory() { |
| return multiModuleProjectDirectory(userDir()); |
| } |
| |
| public Path multiModuleProjectDirectory(Path projectDir) { |
| return value(Environment.MAVEN_MULTIMODULE_PROJECT_DIRECTORY) |
| .orSystemProperty() |
| .orDefault(() -> findDefaultMultimoduleProjectDirectory(projectDir)) |
| .asPath() |
| .toAbsolutePath().normalize(); |
| } |
| |
| public Path logbackConfigurationPath() { |
| return property(Environment.MVND_LOGBACK) |
| .orDefault(() -> mvndHome().resolve("conf/logback.xml").toString()) |
| .orFail() |
| .asPath(); |
| } |
| |
| public String minHeapSize() { |
| return property(Environment.MVND_MIN_HEAP_SIZE).asString(); |
| } |
| |
| public String maxHeapSize() { |
| return property(Environment.MVND_MAX_HEAP_SIZE).asString(); |
| } |
| |
| public String threadStackSize() { |
| return property(Environment.MVND_THREAD_STACK_SIZE).asString(); |
| } |
| |
| public String jvmArgs() { |
| return property(Environment.MVND_JVM_ARGS).asString(); |
| } |
| |
| public String jdkJavaOpts() { |
| return property(Environment.JDK_JAVA_OPTIONS).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 path to {@code pom.xml} or {@code null} |
| */ |
| public Path file() { |
| return value(Environment.MAVEN_FILE).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(); |
| } |
| |
| /** |
| * @return <code>true</code> if maven should be executed within this process instead of spawning a daemon. |
| */ |
| public boolean noDaemon() { |
| return value(Environment.MVND_NO_DAEMON) |
| .orSystemProperty() |
| .orEnvironmentVariable() |
| .orDefault() |
| .asBoolean(); |
| } |
| |
| /** |
| * |
| * @return if mvnd should behave as maven |
| */ |
| public boolean serial() { |
| return value(Environment.SERIAL) |
| .orSystemProperty() |
| .orDefault() |
| .asBoolean(); |
| } |
| |
| /** |
| * @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) { |
| return derive(b -> b.put(Environment.USER_DIR, newUserDir)); |
| } |
| |
| public DaemonParameters withJdkJavaOpts(String opts, boolean before) { |
| String org = this.properties.getOrDefault(Environment.JDK_JAVA_OPTIONS.getProperty(), ""); |
| return derive(b -> b.put(Environment.JDK_JAVA_OPTIONS, |
| org.isEmpty() ? opts : before ? opts + " " + org : org + " " + opts)); |
| } |
| |
| public DaemonParameters withJvmArgs(String opts, boolean before) { |
| String org = this.properties.getOrDefault(Environment.MVND_JVM_ARGS.getProperty(), ""); |
| return derive(b -> b.put(Environment.MVND_JVM_ARGS, |
| org.isEmpty() ? opts : before ? opts + " " + org : org + " " + opts)); |
| } |
| |
| protected DaemonParameters derive(Consumer<PropertiesBuilder> customizer) { |
| PropertiesBuilder builder = new PropertiesBuilder().putAll(this.properties); |
| customizer.accept(builder); |
| return new DaemonParameters(builder); |
| } |
| |
| public Duration keepAlive() { |
| return property(Environment.MVND_KEEP_ALIVE).orFail().asDuration(); |
| } |
| |
| public int maxLostKeepAlive() { |
| return property(Environment.MVND_MAX_LOST_KEEP_ALIVE).orFail().asInt(); |
| } |
| |
| public boolean noBuffering() { |
| return property(Environment.MVND_NO_BUFERING).orFail().asBoolean(); |
| } |
| |
| public int rollingWindowSize() { |
| return property(Environment.MVND_ROLLING_WINDOW_SIZE).orFail().asInt(); |
| } |
| |
| public Duration purgeLogPeriod() { |
| return property(Environment.MVND_LOG_PURGE_PERIOD).orFail().asDuration(); |
| } |
| |
| public Optional<SocketFamily> socketFamily() { |
| return property(Environment.MVND_SOCKET_FAMILY).asOptional().map(SocketFamily::valueOf); |
| } |
| |
| 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.get(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.MVND_EXT_CLASSPATH) { |
| List<String> cp = parseExtClasspath(userHome()); |
| return String.join(",", cp); |
| } else if (env == Environment.MVND_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.getDefault(); |
| } |
| } |
| |
| 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); |
| Properties sysProps = new Properties(); |
| sysProps.putAll(System.getProperties()); |
| System.getenv().forEach((k, v) -> sysProps.put(k, ENV_PREFIX + v)); |
| InterpolationHelper.performSubstitution(result, sysProps::getProperty, true, true); |
| } catch (IOException e) { |
| throw new RuntimeException("Could not read " + path); |
| } |
| } |
| return result; |
| } |
| |
| public static class PropertiesBuilder { |
| private Map<String, String> properties = new LinkedHashMap<>(); |
| |
| public PropertiesBuilder put(Environment envKey, Object value) { |
| if (value == null) { |
| properties.remove(envKey.getProperty()); |
| } else { |
| properties.put(envKey.getProperty(), value.toString()); |
| } |
| return this; |
| } |
| |
| public PropertiesBuilder putAll(Map<String, String> props) { |
| properties.putAll(props); |
| return this; |
| } |
| |
| public Map<String, String> build() { |
| Map<String, String> props = properties; |
| properties = null; |
| return Collections.unmodifiableMap(props); |
| } |
| } |
| |
| /** |
| * 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::getDefault); |
| } |
| |
| 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, () -> { |
| throw couldNotgetValue(); |
| })); |
| } |
| |
| IllegalStateException couldNotgetValue() { |
| EnvValue val = this; |
| 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); |
| while (val != null) { |
| description = description.compose(val.valueSource.descriptionFunction); |
| val = val.previous; |
| if (val != null) { |
| description = description.compose(s -> s.append(", ")); |
| } |
| } |
| description.apply(sb); |
| return 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.isTraceEnabled()) { |
| StringBuilder sb = new StringBuilder("Loaded environment value for key [") |
| .append(envKey.name()) |
| .append("] from "); |
| valueSource.descriptionFunction.apply(sb); |
| sb.append(": [") |
| .append(result) |
| .append(']'); |
| LOG.trace(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() { |
| final String val = get(); |
| return "".equals(val) || Boolean.parseBoolean(val); |
| } |
| |
| public int asInt() { |
| return Integer.parseInt(get()); |
| } |
| |
| public int asInt(IntUnaryOperator function) { |
| return function.applyAsInt(asInt()); |
| } |
| |
| public Duration asDuration() { |
| return TimeUtils.toDuration(get()); |
| } |
| |
| public boolean isSet() { |
| if (get() != null) { |
| return true; |
| } else if (envKey.isOptional()) { |
| return false; |
| } else { |
| throw couldNotgetValue(); |
| } |
| } |
| |
| } |
| } |