| /* |
| * Copyright 2020 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.common; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.time.Duration; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| /** |
| * Collects system properties and environment variables used by mvnd client or server. |
| * |
| * Duration properties such as {@link #MVND_IDLE_TIMEOUT}, {@link #MVND_KEEP_ALIVE}, |
| * {@link #MVND_EXPIRATION_CHECK_DELAY} or {@link #MVND_LOG_PURGE_PERIOD} are expressed |
| * in a human readable format such as <code>2h30m</code>, <code>600ms</code> or <code>10 seconds</code>. |
| * The available units are <i>d/day/days</i>, <i>h/hour/hours</i>, <i>m/min/minute/minutes</i>, |
| * <i>s/sec/second/seconds</i> and <i>ms/millis/msec/milliseconds</i>. |
| */ |
| public enum Environment { |
| |
| /** |
| * Print the completion for the given shell to stdout. Only <code>--completion bash</code> is supported at this time. |
| */ |
| COMPLETION(null, null, null, OptionType.STRING, Flags.OPTIONAL, "mvnd:--completion"), |
| /** |
| * Delete log files under the <code>mvnd.registry</code> directory that are older than <code>mvnd.logPurgePeriod</code> |
| */ |
| PURGE(null, null, null, OptionType.VOID, Flags.OPTIONAL, "mvnd:--purge"), |
| /** Prints the status of daemon instances registered in the registry specified by <code>mvnd.registry</code> */ |
| STATUS(null, null, null, OptionType.VOID, Flags.OPTIONAL, "mvnd:--status"), |
| /** Stop all daemon instances registered in the registry specified by <code>mvnd.registry</code> */ |
| STOP(null, null, null, OptionType.VOID, Flags.OPTIONAL, "mvnd:--stop"), |
| /** Use one thread, no log buffering and the default project builder to behave like a standard maven */ |
| SERIAL("mvnd.serial", null, Boolean.FALSE, OptionType.VOID, Flags.OPTIONAL, "mvnd:-1", "mvnd:--serial"), |
| |
| // |
| // Log properties |
| // |
| |
| /** |
| * The location of the Logback configuration file the daemon should use to configure its logging. |
| */ |
| MVND_LOGBACK("mvnd.logback", null, null, OptionType.PATH, Flags.NONE), |
| /** The system property expected by logback to set the configuration file */ |
| LOGBACK_CONFIGURATION_FILE("logback.configurationFile", null, null, OptionType.PATH, Flags.INTERNAL), |
| |
| // |
| // System properties |
| // |
| /** Java home for starting the daemon */ |
| JAVA_HOME("java.home", "JAVA_HOME", null, OptionType.PATH, Flags.NONE), |
| /** |
| * The daemon installation directory. The client normally sets this according to where its <code>mvnd</code> |
| * executable is located |
| */ |
| MVND_HOME("mvnd.home", "MVND_HOME", null, OptionType.PATH, Flags.DISCRIMINATING), |
| /** The user home directory */ |
| USER_HOME("user.home", null, null, OptionType.PATH, Flags.NONE), |
| /** The current working directory */ |
| USER_DIR("user.dir", null, null, OptionType.PATH, Flags.NONE), |
| /** The JDK_JAVA_OPTIONS option */ |
| JDK_JAVA_OPTIONS("jdk.java.options", "JDK_JAVA_OPTIONS", "", OptionType.STRING, Flags.DISCRIMINATING) { |
| |
| }, |
| |
| // |
| // Maven properties |
| // |
| /** The path to the Maven local repository */ |
| MAVEN_REPO_LOCAL("maven.repo.local", null, null, OptionType.PATH, Flags.NONE), |
| /** The location of the maven settings file */ |
| MAVEN_SETTINGS("maven.settings", null, null, OptionType.PATH, Flags.NONE, "mvn:-s", "mvn:--settings"), |
| /** The pom or directory to build */ |
| MAVEN_FILE(null, null, null, OptionType.PATH, Flags.NONE, "mvn:-f", "mvn:--file"), |
| /** The root directory of the current multi module Maven project */ |
| MAVEN_MULTIMODULE_PROJECT_DIRECTORY("maven.multiModuleProjectDirectory", null, null, OptionType.PATH, Flags.NONE), |
| /** Log file */ |
| MAVEN_LOG_FILE(null, null, null, OptionType.PATH, Flags.INTERNAL, "mvn:-l", "mvn:--log-file"), |
| /** Batch mode */ |
| MAVEN_BATCH_MODE(null, null, null, OptionType.BOOLEAN, Flags.INTERNAL, "mvn:-B", "mvn:--batch-mode"), |
| /** Debug */ |
| MAVEN_DEBUG(null, null, null, OptionType.BOOLEAN, Flags.INTERNAL, "mvn:-X", "mvn:--debug"), |
| /** Version */ |
| MAVEN_VERSION(null, null, null, OptionType.BOOLEAN, Flags.INTERNAL, "mvn:-v", "mvn:-version", "mvn:--version"), |
| /** Show version */ |
| MAVEN_SHOW_VERSION(null, null, null, OptionType.BOOLEAN, Flags.INTERNAL, "mvn:-V", "mvn:--show-version"), |
| /** Define */ |
| MAVEN_DEFINE(null, null, null, OptionType.STRING, Flags.INTERNAL, "mvn:-D", "mvn:--define"), |
| /** Whether the output should be styled using ANSI color codes; possible values: auto, always, never */ |
| MAVEN_COLOR("style.color", null, "auto", OptionType.STRING, Flags.OPTIONAL, "mvnd:--color"), |
| |
| // |
| // mvnd properties |
| // |
| |
| /** |
| * The location of the user supplied <code>mvnd.properties</code> file. |
| */ |
| MVND_PROPERTIES_PATH("mvnd.propertiesPath", "MVND_PROPERTIES_PATH", null, OptionType.PATH, Flags.NONE), |
| /** |
| * The directory under which the daemon stores its registry, log files, etc. |
| * Default: <code>${user.home}/.m2/mvnd</code> |
| */ |
| MVND_DAEMON_STORAGE("mvnd.daemonStorage", null, null, OptionType.PATH, Flags.NONE), |
| /** |
| * The path to the daemon registry. |
| * Default: <code>${mvnd.daemonStorage}/registry.bin</code> |
| */ |
| MVND_REGISTRY("mvnd.registry", null, null, OptionType.PATH, Flags.NONE), |
| /** |
| * If <code>true</code> the log messages are displayed continuously like with stock Maven; otherwise buffer the |
| * messages and output at the end of the build, grouped by module. Passing <code>-B</code> or |
| * <code>--batch-mode</code> on the command line enables this too for the given build. |
| */ |
| MVND_NO_BUFERING("mvnd.noBuffering", null, Boolean.FALSE, OptionType.BOOLEAN, Flags.NONE), |
| /** |
| * The number of log lines to display for each Maven module that is built in parallel. The value can be increased |
| * or decreased by pressing + or - key during the build respectively. This option has no effect with |
| * <code>-Dmvnd.noBuffering=true</code>, <code>-B</code> or <code>--batch-mode</code>. |
| */ |
| MVND_ROLLING_WINDOW_SIZE("mvnd.rollingWindowSize", null, "0", OptionType.INTEGER, Flags.NONE), |
| /** |
| * Daemon log files older than this value will be removed automatically. |
| */ |
| MVND_LOG_PURGE_PERIOD("mvnd.logPurgePeriod", null, "7 days", OptionType.DURATION, Flags.NONE), |
| /** |
| * If <code>true</code>, the client and daemon will run in the same JVM that exits when the build is finished; |
| * otherwise the client starts/connects to a long living daemon process. This option is only available with |
| * non-native clients and is useful mostly for debugging. |
| */ |
| MVND_NO_DAEMON("mvnd.noDaemon", "MVND_NO_DAEMON", Boolean.FALSE, OptionType.BOOLEAN, Flags.DISCRIMINATING), |
| /** |
| * If <code>true</code>, the daemon will be launched in debug mode with the following JVM argument: |
| * <code>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000</code>; otherwise the debug argument is |
| * not passed to the daemon. |
| */ |
| MVND_DEBUG("mvnd.debug", null, Boolean.FALSE, OptionType.BOOLEAN, Flags.DISCRIMINATING), |
| /** |
| * A time period after which an unused daemon will terminate by itself. |
| */ |
| MVND_IDLE_TIMEOUT("mvnd.idleTimeout", null, "3 hours", OptionType.DURATION, Flags.DISCRIMINATING), |
| /** |
| * If the daemon does not send any message to the client in this period of time, send a keep-alive message so that |
| * the client knows that the daemon is still alive. |
| */ |
| MVND_KEEP_ALIVE("mvnd.keepAlive", null, "100 ms", OptionType.DURATION, Flags.DISCRIMINATING), |
| /** |
| * The maximum number of keep alive messages that can be missed by the client before the client considers the daemon |
| * to be dead. |
| */ |
| MVND_MAX_LOST_KEEP_ALIVE("mvnd.maxLostKeepAlive", null, 30, OptionType.INTEGER, Flags.NONE), |
| /** |
| * The minimum number of threads to use when constructing the default <code>-T</code> parameter for the daemon. |
| * This value is ignored if the user passes <code>-T</code>, <code>--threads</code> or <code>-Dmvnd.threads</code> |
| * on the command line or if he sets <code>mvnd.threads</code> in <code>~/.m2/mvnd.properties</code>. |
| */ |
| MVND_MIN_THREADS("mvnd.minThreads", null, 1, OptionType.INTEGER, Flags.NONE), |
| /** |
| * The number of threads to pass to the daemon; same syntax as Maven's <code>-T</code>/<code>--threads</code> |
| * option. |
| */ |
| MVND_THREADS("mvnd.threads", null, null, OptionType.STRING, Flags.NONE, "mvn:-T", "mvn:--threads"), |
| /** |
| * The builder implementation the daemon should use |
| */ |
| MVND_BUILDER("mvnd.builder", null, "smart", OptionType.STRING, Flags.NONE, "mvn:-b", "mvn:--builder"), |
| /** |
| * An ID for a newly started daemon |
| */ |
| MVND_ID("mvnd.id", null, null, OptionType.STRING, Flags.INTERNAL), |
| /** |
| * Internal option to specify the maven extension classpath |
| */ |
| MVND_EXT_CLASSPATH("mvnd.extClasspath", null, null, OptionType.STRING, Flags.DISCRIMINATING | Flags.INTERNAL), |
| /** |
| * Internal option to specify the list of maven extension to register |
| */ |
| MVND_CORE_EXTENSIONS("mvnd.coreExtensions", null, null, OptionType.STRING, Flags.DISCRIMINATING | Flags.INTERNAL), |
| /** |
| * The <code>-Xms</code> value to pass to the daemon |
| */ |
| MVND_MIN_HEAP_SIZE("mvnd.minHeapSize", null, "128M", OptionType.MEMORY_SIZE, Flags.DISCRIMINATING), |
| /** |
| * The <code>-Xmx</code> value to pass to the daemon |
| */ |
| MVND_MAX_HEAP_SIZE("mvnd.maxHeapSize", null, "2G", OptionType.MEMORY_SIZE, Flags.DISCRIMINATING), |
| /** |
| * The <code>-Xss</code> value to pass to the daemon |
| */ |
| MVND_THREAD_STACK_SIZE("mvnd.threadStackSize", null, "1M", OptionType.MEMORY_SIZE, Flags.DISCRIMINATING), |
| /** |
| * Additional JVM args to pass to the daemon |
| */ |
| MVND_JVM_ARGS("mvnd.jvmArgs", null, null, OptionType.STRING, Flags.DISCRIMINATING | Flags.OPTIONAL), |
| /** |
| * If <code>true</code>, the <code>-ea</code> option will be passed to the daemon; otherwise the <code>-ea</code> |
| * option is not passed to the daemon. |
| */ |
| MVND_ENABLE_ASSERTIONS("mvnd.enableAssertions", null, Boolean.FALSE, OptionType.BOOLEAN, Flags.DISCRIMINATING), |
| /** |
| * The daemon will check this often whether it should exit. |
| */ |
| MVND_EXPIRATION_CHECK_DELAY("mvnd.expirationCheckDelay", null, "10 seconds", OptionType.DURATION, Flags.DISCRIMINATING), |
| /** |
| * Period after which idle duplicate daemons will be shut down. Duplicate daemons are daemons with the same set of |
| * discriminating start parameters. |
| */ |
| MVND_DUPLICATE_DAEMON_GRACE_PERIOD("mvnd.duplicateDaemonGracePeriod", null, "10 seconds", OptionType.DURATION, |
| Flags.DISCRIMINATING), |
| /** |
| * Internal property to tell the daemon the width of the terminal |
| */ |
| MVND_TERMINAL_WIDTH("mvnd.terminalWidth", null, 0, OptionType.INTEGER, Flags.INTERNAL), |
| /** |
| * Internal property to tell the daemon which JAVA_HOME was used to start it. It needs to be passed explicitly |
| * because the value may differ from what the daemon sees through <code>System.getProperty("java.home")</code>. |
| */ |
| MVND_JAVA_HOME("mvnd.java.home", null, null, OptionType.PATH, Flags.INTERNAL), |
| /** |
| * Log mojos execution time at the end of the build. |
| */ |
| MVND_BUILD_TIME("mvnd.buildTime", null, null, OptionType.BOOLEAN, Flags.NONE), |
| /** |
| * Socket family to use |
| */ |
| MVND_SOCKET_FAMILY("mvnd.socketFamily", null, "inet", OptionType.STRING, Flags.DISCRIMINATING), |
| /** |
| * Pattern that will force eviction of the plugin realms if one of its dependencies matches. |
| * The overall pattern is a comma separated list of either: |
| * <ul> |
| * <li>a glob pattern starting with <code>'glob:'</code> (the default syntax if no scheme is specified),</li> |
| * <li>a regex pattern starting with <code>'regex:'</code>,</li> |
| * <li>a maven expression, either <code>'mvn:[groupId]:[artifactId]:[version]'</code>, |
| * <code>'mvn:[groupId]:[artifactId]'</code> or <code>'mvn:[artifactId]</code>'.</li> |
| * </ul> |
| * This pattern will be evaluated against the full path of the dependencies, so it is usually desirable to |
| * start with <code>'glob:**/'</code> to support any location of the local repository. |
| */ |
| MVND_PLUGIN_REALM_EVICT_PATTERN("mvnd.pluginRealmEvictPattern", null, "", OptionType.STRING, Flags.OPTIONAL), |
| /** |
| * The SyncContextFactory to use (can be either 'noop' or 'ipc' for a server-wide factory). |
| */ |
| MVND_SYNC_CONTEXT_FACTORY("mvnd.syncContextFactory", null, "local", OptionType.BOOLEAN, Flags.OPTIONAL); |
| |
| static Properties properties; |
| |
| public static void setProperties(Properties properties) { |
| Environment.properties = properties; |
| } |
| |
| public static String getProperty(String property) { |
| Properties props = Environment.properties; |
| if (props == null) { |
| props = System.getProperties(); |
| } |
| return props.getProperty(property); |
| } |
| |
| private final String property; |
| private final String environmentVariable; |
| private final String default_; |
| private final int flags; |
| private final OptionType type; |
| private final Map<String, OptionOrigin> options; |
| |
| Environment(String property, String environmentVariable, Object default_, OptionType type, int flags, |
| String... options) { |
| if (property == null && options.length == 0) { |
| throw new IllegalArgumentException( |
| "An " + Environment.class.getSimpleName() + " entry must have property or options set"); |
| } |
| this.property = property; |
| this.environmentVariable = environmentVariable; |
| this.default_ = default_ != null ? default_.toString() : null; |
| this.flags = flags; |
| this.type = type; |
| if (options.length == 0) { |
| this.options = Collections.emptyMap(); |
| } else { |
| final Map<String, OptionOrigin> optMap = new LinkedHashMap<>(); |
| for (String opt : options) { |
| OPTION_ORIGIN_SEARCH: { |
| for (OptionOrigin oo : OptionOrigin.values()) { |
| if (opt.startsWith(oo.prefix)) { |
| optMap.put(opt.substring(oo.prefix.length()), oo); |
| break OPTION_ORIGIN_SEARCH; |
| } |
| } |
| throw new IllegalArgumentException( |
| "Unexpected option prefix: '" + opt + "'; Options should start with any of " |
| + Stream.of(OptionOrigin.values()).map(oo -> oo.prefix).collect(Collectors.joining(","))); |
| } |
| } |
| this.options = Collections.unmodifiableMap(optMap); |
| } |
| } |
| |
| public String getProperty() { |
| return property; |
| } |
| |
| public String getEnvironmentVariable() { |
| return environmentVariable; |
| } |
| |
| public String getDefault() { |
| return default_; |
| } |
| |
| public Set<String> getOptions() { |
| return options.keySet(); |
| } |
| |
| public Map<String, OptionOrigin> getOptionMap() { |
| return options; |
| } |
| |
| public OptionType getType() { |
| return type; |
| } |
| |
| public boolean isDiscriminating() { |
| return (flags & Flags.DISCRIMINATING) != 0; |
| } |
| |
| public boolean isInternal() { |
| return (flags & Flags.INTERNAL) != 0; |
| } |
| |
| public boolean isOptional() { |
| return (flags & Flags.OPTIONAL) != 0; |
| } |
| |
| public String asString() { |
| String val = getProperty(property); |
| if (val == null) { |
| throw new IllegalStateException("The system property " + property + " is missing"); |
| } |
| return val; |
| } |
| |
| public Optional<String> asOptional() { |
| String val = getProperty(property); |
| if (val != null) { |
| return Optional.of(val); |
| } else if (isOptional()) { |
| return Optional.empty(); |
| } else { |
| throw new IllegalStateException("The system property " + property + " is missing"); |
| } |
| } |
| |
| public int asInt() { |
| return Integer.parseInt(asString()); |
| } |
| |
| public boolean asBoolean() { |
| return Boolean.parseBoolean(asString()); |
| } |
| |
| public Path asPath() { |
| String result = asString(); |
| if (Os.current().isCygwin()) { |
| result = cygpath(result); |
| } |
| return Paths.get(result); |
| } |
| |
| public Duration asDuration() { |
| return TimeUtils.toDuration(asString()); |
| } |
| |
| public String asDaemonOpt(String value) { |
| return property + "=" + type.normalize(value); |
| } |
| |
| public void addCommandLineOption(Collection<String> args, String value) { |
| if (!options.isEmpty()) { |
| args.add(options.keySet().iterator().next()); |
| args.add(type.normalize(value)); |
| } else { |
| args.add("-D" + property + "=" + type.normalize(value)); |
| } |
| } |
| |
| public boolean hasCommandLineOption(Collection<String> args) { |
| final String[] prefixes = getPrefixes(); |
| return args.stream().anyMatch(arg -> Stream.of(prefixes).anyMatch(arg::startsWith)); |
| } |
| |
| public String getCommandLineOption(Collection<String> args) { |
| return getCommandLineOption(args, false); |
| } |
| |
| public String removeCommandLineOption(Collection<String> args) { |
| return getCommandLineOption(args, true); |
| } |
| |
| String getCommandLineOption(Collection<String> args, boolean remove) { |
| final String[] prefixes = getPrefixes(); |
| String value = null; |
| for (Iterator<String> it = args.iterator(); it.hasNext();) { |
| String arg = it.next(); |
| if (Stream.of(prefixes).anyMatch(arg::startsWith)) { |
| if (remove) { |
| it.remove(); |
| } |
| if (type == OptionType.VOID) { |
| value = ""; |
| } else { |
| String opt = Stream.of(prefixes).filter(arg::startsWith) |
| .max(Comparator.comparing(String::length)).get(); |
| value = arg.substring(opt.length()); |
| if (value.isEmpty()) { |
| if (it.hasNext()) { |
| value = it.next(); |
| if (remove) { |
| it.remove(); |
| } |
| } |
| } else { |
| if (value.charAt(0) == '=') { |
| value = value.substring(1); |
| } |
| } |
| } |
| } |
| } |
| return value; |
| } |
| |
| private String[] getPrefixes() { |
| final String[] prefixes; |
| if (options.isEmpty()) { |
| prefixes = new String[] { "-D" + property + "=" }; |
| } else if (property != null) { |
| prefixes = new String[options.size() + 1]; |
| options.keySet().toArray(prefixes); |
| prefixes[options.size()] = "-D" + property + "="; |
| } else { |
| prefixes = options.keySet().toArray(new String[0]); |
| } |
| return prefixes; |
| } |
| |
| public static String cygpath(String result) { |
| String path = result.replace('/', '\\'); |
| if (path.matches("\\\\cygdrive\\\\[a-z]\\\\.*")) { |
| String s = path.substring("\\cygdrive\\".length()); |
| result = s.substring(0, 1).toUpperCase(Locale.ENGLISH) + ":" + s.substring(1); |
| } |
| return result; |
| } |
| |
| public static boolean isNative() { |
| return "executable".equals(System.getProperty("org.graalvm.nativeimage.kind")); |
| } |
| |
| public static Stream<DocumentedEnumEntry<Environment>> documentedEntries() { |
| Properties props = new Properties(); |
| Environment[] values = values(); |
| final String cliOptionsPath = values[0].getClass().getSimpleName() + ".javadoc.properties"; |
| try (InputStream in = Environment.class.getResourceAsStream(cliOptionsPath)) { |
| props.load(in); |
| } catch (IOException e) { |
| throw new RuntimeException("Could not read " + cliOptionsPath, e); |
| } |
| return Stream.of(values) |
| .filter(env -> !env.isInternal()) |
| .sorted(Comparator.<Environment, String> comparing(env -> env.property != null ? env.property : "") |
| .thenComparing(env -> !env.options.isEmpty() ? env.options.keySet().iterator().next() : "")) |
| .map(env -> new DocumentedEnumEntry<>(env, props.getProperty(env.name()))); |
| } |
| |
| public enum OptionOrigin { |
| mvn, mvnd; |
| |
| private final String prefix; |
| |
| private OptionOrigin() { |
| this.prefix = name() + ":"; |
| } |
| } |
| |
| /** |
| * The values of {@link Environment#MAVEN_COLOR} option. |
| */ |
| public enum Color { |
| always, never, auto; |
| |
| public static Optional<Color> of(String color) { |
| return color == null ? Optional.empty() : Optional.of(Color.valueOf(color)); |
| } |
| } |
| |
| public static class DocumentedEnumEntry<E> { |
| |
| private final E entry; |
| private final String javaDoc; |
| |
| public DocumentedEnumEntry(E entry, String javaDoc) { |
| this.entry = entry; |
| this.javaDoc = javaDoc; |
| } |
| |
| public E getEntry() { |
| return entry; |
| } |
| |
| public String getJavaDoc() { |
| return javaDoc; |
| } |
| } |
| |
| static class Flags { |
| private static final int NONE = 0b0; |
| private static final int DISCRIMINATING = 0b1; |
| private static final int INTERNAL = 0b10; |
| private static final int OPTIONAL = 0b100; |
| } |
| |
| } |