All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graalvm.launcher.Launcher Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.graalvm.launcher;

import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.WRITE;

import java.io.BufferedOutputStream;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;

import org.graalvm.collections.Pair;
import org.graalvm.home.HomeFinder;
import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.ProcessProperties;
import org.graalvm.nativeimage.RuntimeOptions;
import org.graalvm.nativeimage.VMRuntime;
import org.graalvm.options.OptionCategory;
import org.graalvm.options.OptionDescriptor;
import org.graalvm.options.OptionStability;
import org.graalvm.shadowed.org.jline.terminal.Terminal;
import org.graalvm.shadowed.org.jline.terminal.TerminalBuilder;

public abstract class Launcher {
    private static final boolean STATIC_VERBOSE = Boolean.getBoolean("org.graalvm.launcher.verbose");
    private static final boolean SHELL_SCRIPT_LAUNCHER = Boolean.getBoolean("org.graalvm.launcher.shell");

    /**
     * Default option description indentation.
     */
    public static final int LAUNCHER_OPTIONS_INDENT = 45;

    static final boolean IS_AOT = Boolean.getBoolean("com.oracle.graalvm.isaot");
    @Deprecated(since = "22.2", forRemoval = true) private static final String HELP_INTERNAL = "--help:internal";
    @Deprecated(since = "22.2", forRemoval = true) private static final String HELP_EXPERT = "--help:expert";

    private static final String EXECUTABLE_NAME_PROPERTY = "org.graalvm.launcher.executablename";

    public enum VMType {
        Native,
        JVM
    }

    final Native nativeAccess;
    private final boolean verbose;
    private PrintStream out = System.out;
    private PrintStream err = System.err;

    String helpArg = null;
    boolean helpInternal;
    boolean helpPrinted;

    /**
     * Path to the desired log file, or {@code null} if no log redirection is required.
     */
    private Path logFile;

    /**
     * Number of spaces reserved for the options column. Can be set separately for each option
     * block.
     */
    private int optionIndent = LAUNCHER_OPTIONS_INDENT;

    /**
     * Accumulates help categories and their relevant options.
     *
     * @see #printOtherHelpCategory
     */
    private List kindAndCategory = new ArrayList<>();

    protected enum VersionAction {
        None,
        PrintAndExit,
        PrintAndContinue
    }

    protected Launcher() {
        verbose = STATIC_VERBOSE || Boolean.parseBoolean(System.getenv("VERBOSE_GRAALVM_LAUNCHERS"));
        if (IS_AOT) {
            nativeAccess = new Native();
        } else {
            nativeAccess = null;
        }
    }

    /**
     * Provides the name of the log file, if specified on the command line.
     *
     * @return log file Path. {@code null} if unspecified.
     * @since 20.0
     */
    protected final Path getLogFile() {
        return logFile;
    }

    /**
     * Uses the defined output to print messages.
     *
     * @param ps printStream to use as out
     * @since 20.0
     */
    protected final void setOutput(PrintStream ps) {
        this.out = ps;
    }

    /**
     * Uses the defined output to print error messages.
     *
     * @param ps printStream to use as err
     * @since 20.0
     */
    protected final void setError(PrintStream ps) {
        this.err = ps;
    }

    /**
     * @return the stream for regular output. Defaults to {@link System#out}
     * @since 20.0
     */
    protected final PrintStream getOutput() {
        return out;
    }

    /**
     * @return the stream for errors. Defaults to {@link System#err}
     * @since 20.0
     */
    protected final PrintStream getError() {
        return err;
    }

    void handleAbortException(AbortException e) {
        if (e.getMessage() != null) {
            err.println("ERROR: " + e.getMessage());
        }
        if (e.getCause() != null) {
            e.printStackTrace();
        }
        System.exit(e.getExitCode());
    }

    /**
     * Exception which shall abort the launcher execution. Thrown by this class in the case of
     * unhandled internal exception, malformed arguments or unknown options, or deliberate exit.
     *
     * @since 20.0
     */
    protected static final class AbortException extends RuntimeException {
        static final long serialVersionUID = 4681646279864737876L;
        private final int exitCode;

        AbortException(String message, int exitCode) {
            super(message);
            this.exitCode = exitCode;
        }

        AbortException(Throwable cause, int exitCode) {
            super(null, cause);
            this.exitCode = exitCode;
        }

        public int getExitCode() {
            return exitCode;
        }

        @SuppressWarnings("sync-override")
        @Override
        public Throwable fillInStackTrace() {
            return this;
        }
    }

    /**
     * Exits the launcher, indicating success.
     *
     * This exits by throwing an {@link AbortException}.
     */
    protected final AbortException exit() {
        return exit(0);
    }

    /**
     * Exits the launcher with the provided exit code.
     *
     * This exits by throwing an {@link AbortException}.
     *
     * @param exitCode the exit code of the launcher process.
     */
    protected final AbortException exit(int exitCode) {
        return abort((String) null, exitCode);
    }

    /**
     * Exits the launcher, indicating failure.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param message an error message that will be printed to {@linkplain System#err stderr}. If
     *            null, nothing will be printed.
     */
    protected final AbortException abort(String message) {
        return abort(message, 1);
    }

    /**
     * Exits the launcher, with the provided exit code.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param message an error message that will be printed to {@linkplain System#err stderr}. If
     *            null, nothing will be printed.
     * @param exitCode the exit code of the launcher process.
     */
    @SuppressWarnings("static-method")
    protected final AbortException abort(String message, int exitCode) {
        throw new AbortException(message, exitCode);
    }

    /**
     * Exits the launcher, indicating failure because of the provided {@link Throwable}.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param t the exception that causes the launcher to abort.
     */
    protected final AbortException abort(Throwable t) {
        return abort(t, 255);
    }

    /**
     * Exits the launcher with the provided exit code because of the provided {@link Throwable}.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param t the exception that causes the launcher to abort.
     * @param exitCode the exit code of the launcher process.
     */
    protected final AbortException abort(Throwable t, int exitCode) {
        if (t.getCause() instanceof IOException && t.getClass() == RuntimeException.class) {
            String message = t.getMessage();
            if (message != null && !message.startsWith(t.getCause().getClass().getName() + ": ")) {
                err.println(message);
            }
            throw abort((IOException) t.getCause(), exitCode);
        }
        throw new AbortException(t, exitCode);
    }

    /**
     * Exits the launcher, indicating failure because of the provided {@link IOException}.
     *
     * This tries to build a helpful error message based on exception.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param e the exception that causes the launcher to abort.
     */
    protected final AbortException abort(IOException e) {
        return abort(e, 74);
    }

    /**
     * Exits the launcher with the provided exit code because of the provided {@link IOException}.
     *
     * This tries to build a helpful error message based on exception.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param e the exception that causes the launcher to abort.
     * @param exitCode the exit code of the launcher process
     */
    protected final AbortException abort(IOException e, int exitCode) {
        String message = e.getMessage();
        if (message != null) {
            if (e instanceof NoSuchFileException) {
                throw abort("No such file: " + message, exitCode);
            } else if (e instanceof AccessDeniedException) {
                throw abort("Access denied: " + message, exitCode);
            } else {
                throw abort(message + " (" + e.getClass().getSimpleName() + ")", exitCode);
            }
        }
        throw abort((Throwable) e, exitCode);
    }

    /**
     * This is called to abort execution when an argument can neither be recognized by the launcher
     * or as an option for the polyglot engine.
     *
     * @param argument the argument that was not recognized.
     */
    protected AbortException abortUnrecognizedArgument(String argument) {
        throw abortInvalidArgument(argument, "Unrecognized argument: '" + argument + "'. Use --help for usage instructions.");
    }

    /**
     * Exits the launcher, indicating failure because of an invalid argument.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param argument the problematic argument.
     * @param message an error message that is printed to {@linkplain System#err stderr}.
     */
    protected final AbortException abortInvalidArgument(String argument, String message) {
        return abortInvalidArgument(argument, message, 2);
    }

    /**
     * Exits the launcher with the provided exit code because of an invalid argument.
     *
     * This aborts by throwing an {@link AbortException}.
     *
     * @param argument the problematic argument.
     * @param message an error message that is printed to {@linkplain System#err stderr}.
     * @param exitCode the exit code of the launcher process.
     */
    protected final AbortException abortInvalidArgument(String argument, String message, int exitCode) {
        Set allArguments = collectAllArguments();
        int equalIndex;
        String testString = argument;
        if ((equalIndex = argument.indexOf('=')) != -1) {
            testString = argument.substring(0, equalIndex);
        }

        List matches = fuzzyMatch(allArguments, testString, 0.7f);
        if (matches.isEmpty()) {
            // try even harder
            matches = fuzzyMatch(allArguments, testString, 0.5f);
        }

        StringBuilder sb = new StringBuilder();
        if (message != null) {
            sb.append(message);
        }
        if (!matches.isEmpty()) {
            if (sb.length() > 0) {
                sb.append(System.lineSeparator());
            }
            sb.append("Did you mean one of the following arguments?").append(System.lineSeparator());
            Iterator iterator = matches.iterator();
            while (true) {
                String match = iterator.next();
                sb.append("  ").append(match);
                if (iterator.hasNext()) {
                    sb.append(System.lineSeparator());
                } else {
                    break;
                }
            }
        }
        if (sb.length() > 0) {
            throw abort(sb.toString(), exitCode);
        } else {
            throw exit(exitCode);
        }
    }

    protected void warn(String message) {
        err.println("Warning: " + message);
    }

    protected void warn(String message, Object... args) {
        StringBuilder sb = new StringBuilder("Warning: ");
        new Formatter(sb).format(message, args);
        sb.append(System.lineSeparator());
        err.print(sb.toString());
    }

    /**
     * Sets the indentation for option descriptions. Sets number of spaces in the first column
     * reserved for option names. Defaults to {@link #LAUNCHER_OPTIONS_INDENT}.
     *
     * @param indent the new indent.
     * @since 20.0
     */
    protected final void setOptionIndent(int indent) {
        if (indent < 0) {
            optionIndent = LAUNCHER_OPTIONS_INDENT;
        } else {
            optionIndent = indent;
        }
    }

    /**
     * Prints a help message to {@linkplain System#out stdout}. This only prints options that belong
     * to categories {@code maxCategory or less}.
     *
     * @param maxCategory the maximum category of options that should be printed.
     */
    protected abstract void printHelp(OptionCategory maxCategory);

    /**
     * Prints version information on {@linkplain System#out stdout}.
     */
    protected abstract void printVersion();

    /**
     * Add all known arguments to the {@code options} list.
     *
     * @param options list to which valid arguments must be added.
     */
    protected abstract void collectArguments(Set options);

    /**
     * Finds the a descriptor for the option.
     *
     * @param group option group
     * @param key the option name (including the group)
     * @return descriptor or {@code null}.
     * @since 20.0
     */
    protected abstract OptionDescriptor findOptionDescriptor(String group, String key);

    /**
     * Determines if the tool supports polyglot. Returns true, if {@code --polyglot} option is valid
     * for this tool and polyglot launcher works for it. The default implementation returns false
     * only when {@link #isStandalone()} is true.
     *
     * @return {@code true}, if polyglot is relevant in this launcher.
     * @since 20.0
     */
    protected boolean canPolyglot() {
        return !isStandalone();
    }

    /**
     * Should print tool-specific help. Regular languages print info on the installed tools and
     * languages. The default implementation prints nothing.
     *
     * @param helpCategory category of options to print
     * @since 20.0
     */
    protected void maybePrintAdditionalHelp(OptionCategory helpCategory) {
        // no op, no additional help printed.
    }

    private String[] executableNames(String basename) {
        switch (OS.current) {
            case Linux:
            case Darwin:
            case Solaris:
                return new String[]{basename};
            case Windows:
                return new String[]{basename + ".exe", basename + ".cmd"};
            default:
                throw abort("executableName: OS not supported: " + OS.current);
        }
    }

    /**
     * Returns the name of the main class for this launcher.
     *
     * Typically:
     *
     * 
     * return MyLauncher.class.getName();
     * 
*/ protected String getMainClass() { return this.getClass().getName(); } /** * The return value specifies the default VM when none of --jvm, --native options is used. * * @return the default VMType */ protected VMType getDefaultVMType() { return VMType.Native; } /** * Returns true if the current launcher was compiled ahead-of-time to native code. */ public static boolean isAOT() { return IS_AOT; } private boolean isVerbose() { return verbose; } protected boolean isGraalVMAvailable() { return getGraalVMHome() != null; } protected boolean isStandalone() { return !isGraalVMAvailable(); } private Path home; protected Path getGraalVMHome() { if (home == null) { home = HomeFinder.getInstance().getHomeFolder(); } return home; } /** * Returns filename of the binary, depending on OS. Binary will be searched in {@code bin} * directory. * * @param binaryName binary name, without path. * @return OS-dependent binary filename. */ protected final Path getGraalVMBinaryPath(String binaryName) { String[] executableNames = executableNames(binaryName); Path graalVMHome = getGraalVMHome(); if (graalVMHome == null) { throw abort("Cannot exec to GraalVM binary: could not find GraalVM home"); } for (String executableName : executableNames) { Path execPath = graalVMHome.resolve("bin").resolve(executableName); if (Files.exists(execPath)) { return execPath; } } throw abort("Cannot exec to GraalVM binary: could not find a '" + binaryName + "' executable"); } /** * Runs launcher's action as version print or help. Returns {@code true}, if the execution * should terminate, e.g. after printing help. {@link #parseCommonOption} should be called for * commandline argument(s) prior to this method to set up flags to display help etc. * * @return {@code true} when execution should be terminated. * @since 20.0 */ protected boolean runLauncherAction() { if (helpArg == null) { return false; } if ("".equals(helpArg)) { helpPrinted = true; printDefaultHelp(OptionCategory.EXPERT); } maybePrintAdditionalHelp(OptionCategory.EXPERT); if ("all".equals(helpArg) || "vm".equals(helpArg)) { helpPrinted = true; out.println(""); if (nativeAccess == null) { printJvmHelp(); } else { nativeAccess.printNativeHelp(); } } if (!helpPrinted) { printDefaultHelp(OptionCategory.USER); } out.println(""); out.println("See http://www.graalvm.org for more information."); return true; } /** * Prints default help text. Prints options, starting with tool specific options. Launcher * implementations can override to provide launcher-specific intro / summary. * * @param printCategory options category to print. * @since 20.0 */ protected void printDefaultHelp(OptionCategory printCategory) { final VMType defaultVMType = SHELL_SCRIPT_LAUNCHER ? VMType.JVM : this.getDefaultVMType(); printHelp(printCategory); out.println(); out.println("Runtime options:"); setOptionIndent(45); if (canPolyglot()) { launcherOption("--polyglot", "Run with all other guest languages accessible."); } if (!SHELL_SCRIPT_LAUNCHER) { launcherOption("--native", "Run using the native launcher with limited access to Java libraries" + (defaultVMType == VMType.Native ? " (default)" : "") + "."); } if (!isStandalone()) { launcherOption("--jvm", "Run on the Java Virtual Machine with access to Java libraries" + (defaultVMType == VMType.JVM ? " (default)" : "") + "."); } // @formatter:off launcherOption("--vm.[option]", "Pass options to the host VM. To see available options, use '--help:vm'."); launcherOption("--log.file=", "Redirect guest languages logging into a given file."); launcherOption("--log.[logger].level=", "Set language log level to OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST or ALL."); launcherOption("--help", "Print this help message."); launcherOption("--help:vm", "Print options for the host VM."); // @formatter:on } /** * Instructs that information about other help categories should be printed. * * @param kind category kind name * @param option the option to print the category * @since 20.0 */ protected void printOtherHelpCategory(String kind, String option) { kindAndCategory.add(kind); kindAndCategory.add(option); } static String optionsTitle(String kind, OptionCategory optionCategory) { if (optionCategory == null) { return kind + " options:"; } String category; switch (optionCategory) { case USER: category = "User "; break; case EXPERT: category = "Expert "; break; case INTERNAL: category = "Internal "; break; default: category = ""; break; } return category + kind + " options:"; } /** * Parses otherwise unrecognized options. Terminates the application if an option is not among * the generic launcher / VM ones. * * @param defaultOptionPrefix (language) prefix for the options * @param polyglotOptions options being built for the polyglot launcher * @param unrecognizedArgs arguments (options) to evaluate * @since 20.0 */ protected final void parseUnrecognizedOptions(String defaultOptionPrefix, Map polyglotOptions, List unrecognizedArgs) { boolean experimentalOptions = false; // First, check if --experimental-options is passed for (String arg : unrecognizedArgs) { switch (arg) { case "--experimental-options": case "--experimental-options=true": experimentalOptions = true; break; case "--experimental-options=false": experimentalOptions = false; break; } } // Parse the arguments, now that we know whether experimental options are allowed for (String arg : unrecognizedArgs) { if (!parseCommonOption(defaultOptionPrefix, polyglotOptions, experimentalOptions, arg)) { parsePolyglotOption(defaultOptionPrefix, polyglotOptions, experimentalOptions, arg); } } } /** * Parses an option, returning success. The method is called to parse `arg` option from the * commandline, not recognized by the application. The method may contribute to the * `polyglotOptions` (in/out parameter, modifiable) to alter polyglot behaviour. If the option * is recognized, the method must return {@code true}. * * @param defaultOptionPrefix default prefix for the option names, derived from the launching * application. * @param polyglotOptions options for polyglot engine * @param experimentalOptions true, if experimental options are explicitly allowed * @param arg argument to parse * @return true, if the option was recognized. * @since 20.0 */ protected boolean parseCommonOption(String defaultOptionPrefix, Map polyglotOptions, boolean experimentalOptions, String arg) { // Ignore, these were already parsed before if ("--experimental-options".equals(arg) || "--experimental-options=false".equals(arg) || "--experimental-options=true".equals(arg)) { return true; } if (arg.startsWith("--help")) { parseHelpArg(arg); return true; } return false; } private void parseHelpArg(String arg) { // TODO: GR-38305 Remove legacy behaviour support if (HELP_EXPERT.equals(arg)) { out.println(""); out.println("NOTE: The " + HELP_EXPERT + " option is deprecated without replacement and will be removed."); out.println(""); if (helpArg == null) { helpArg = ""; } return; } if (HELP_INTERNAL.equals(arg)) { out.println(""); out.println("NOTE: The " + HELP_INTERNAL + " option is deprecated and will be removed. Use --help:[id]:internal instead."); out.println(""); if (helpArg == null) { helpArg = ""; } helpInternal = true; return; } int index = arg.indexOf(':'); if (index < 0) { helpArg = ""; return; } String helpArgCandidate = arg.substring(index + 1); index = helpArgCandidate.indexOf(':'); if (index < 0) { helpArg = helpArgCandidate; return; } helpArg = helpArgCandidate.substring(0, index); if (helpArgCandidate.endsWith(":internal")) { helpInternal = true; } } /** * Parses polyglot options and already-processed --jvm/--native/--vm.* options. For any other * argument, it will abort(). For parameter description see * {@link #parseCommonOption(java.lang.String, java.util.Map, boolean, java.lang.String)}. */ void parsePolyglotOption(String defaultOptionPrefix, Map polyglotOptions, boolean experimentalOptions, String arg) { if (arg.equals("--jvm")) { if (isAOT()) { throw abort("should not reach here: jvm option failed to switch to JVM"); } return; } else if (arg.equals("--native")) { if (!isAOT()) { throw abort("native options are not supported on the JVM"); } return; } else if (arg.startsWith("--vm.") && arg.length() > "--vm.".length()) { /* * Ignore those, they should already be applied by now. They can come from 2 source: * from the Bash/Batch launchers, but those launchers remove all such arguments. Or from * a native launcher which switched from native to jvm, in which case we reuse all * original arguments for correctness and already applied the --vm options by passing * them to the JVM directly. */ return; } // getLanguageId() or null? if (arg.length() <= 2 || !arg.startsWith("--")) { throw abortUnrecognizedArgument(arg); } int eqIdx = arg.indexOf('='); String key; String value; if (eqIdx < 0) { key = arg.substring(2); value = null; } else { key = arg.substring(2, eqIdx); value = arg.substring(eqIdx + 1); } if (value == null) { value = "true"; } int index = key.indexOf('.'); String group = key; if (index >= 0) { group = group.substring(0, index); } if ("log".equals(group)) { if (key.endsWith(".level")) { try { Level.parse(value); polyglotOptions.put(key, value); } catch (IllegalArgumentException e) { throw abort(String.format("Invalid log level %s specified. %s'", arg, e.getMessage())); } return; } else if (key.equals("log.file")) { logFile = Paths.get(value); return; } } OptionDescriptor descriptor = findOptionDescriptor(group, key); if (descriptor == null) { if (defaultOptionPrefix != null) { key = defaultOptionPrefix + "." + key; descriptor = findOptionDescriptor(defaultOptionPrefix, key); } if (descriptor == null) { throw abortUnrecognizedArgument(arg); } } try { descriptor.getKey().getType().convert(value); } catch (IllegalArgumentException e) { throw abort(String.format("Invalid argument %s specified. %s", arg, e.getMessage())); } if (descriptor.isDeprecated()) { String messageFormat = "Option '%s' is deprecated and might be removed from future versions."; String deprecationMessage = descriptor.getDeprecationMessage(); String message; if (deprecationMessage != null) { message = String.format(messageFormat + "%n%s", descriptor.getName(), deprecationMessage); } else { message = String.format(messageFormat, descriptor.getName()); } warn(message); } if (!experimentalOptions && descriptor.getStability() == OptionStability.EXPERIMENTAL) { throw abort(String.format("Option '%s' is experimental and must be enabled via '--experimental-options'%n" + "Do not use experimental options in production environments.", arg)); } polyglotOptions.put(key, value); } private Set collectAllArguments() { Set options = new LinkedHashSet<>(); collectArguments(options); if (canPolyglot()) { options.add("--polyglot"); } if (!isStandalone()) { options.add("--jvm"); } options.add("--native"); options.add("--help"); options.add("--help:vm"); return options; } /** * Returns the set of options that fuzzy match a given option name. */ private static List fuzzyMatch(Set arguments, String argument, float threshold) { List matches = new ArrayList<>(); for (String arg : arguments) { float score = stringSimilarity(arg, argument); if (score >= threshold) { matches.add(arg); } } return matches; } /** * Compute string similarity based on Dice's coefficient. * * Ported from str_similar() in globals.cpp. */ private static float stringSimilarity(String str1, String str2) { int hit = 0; for (int i = 0; i < str1.length() - 1; ++i) { for (int j = 0; j < str2.length() - 1; ++j) { if ((str1.charAt(i) == str2.charAt(j)) && (str1.charAt(i + 1) == str2.charAt(j + 1))) { ++hit; break; } } } return 2.0f * hit / (str1.length() + str2.length()); } /** * Prints a line for a launcher option. Uses indentation set by {@link #setOptionIndent} to * align option's description. If option name is too long, description is printed on the next * line, indented. * * @param option option name, including dash(es) * @param description description * @since 20.0 */ protected void launcherOption(String option, String description) { printOption(option, description, 2, optionIndent); } private static String spaces(int length) { return new String(new char[length]).replace('\0', ' '); } @SuppressWarnings("hiding") private String wrap(String s, String indent) { final int terminalWidth = Math.max(getTerminalWidth(), indent.length() + 10); final int width = terminalWidth - indent.length(); String rest = s.strip(); if (rest.length() <= width) { return rest; } StringBuilder builder = new StringBuilder(); while (rest.length() > width) { // NOTE: may return width, which is fine, we cut just before i int i = rest.lastIndexOf(' ', width); if (i == -1) { // Can't find any space in rest[0 < i <= width] // take the next space (will exceed the width) i = rest.indexOf(' ', width); } if (i != -1) { builder.append(rest, 0, i); builder.append(System.lineSeparator()); builder.append(indent); rest = rest.substring(i + 1).stripLeading(); // + 1 to skip the space } else { break; // No space left in rest } } builder.append(rest); return builder.toString(); } private static final Method IS_TERMINAL_METHOD = getIsTerminalMethod(); private static Method getIsTerminalMethod() { try { return Console.class.getMethod("isTerminal"); } catch (NoSuchMethodException e) { return null; } } /** * Returns true if stdin and stdout are both TTY, false otherwise. * * @since 24.0 */ protected static boolean isTTY() { Console console = System.console(); if (console == null) { return false; } if (IS_TERMINAL_METHOD != null) { try { return (boolean) IS_TERMINAL_METHOD.invoke(console); } catch (IllegalAccessException | InvocationTargetException e) { throw new Error(e); } } else { return true; } } private static final int FALLBACK_TERMINAL_WIDTH = 120; private int terminalWidth = -1; int getTerminalWidth() { if (terminalWidth == -1) { int width; if (isTTY()) { try (Terminal terminal = createSystemTerminal()) { width = terminal.getWidth(); } catch (IOException exception) { width = FALLBACK_TERMINAL_WIDTH; } } else { width = FALLBACK_TERMINAL_WIDTH; } if (width <= 0) { // Dumb terminal width = FALLBACK_TERMINAL_WIDTH; } terminalWidth = width; } return terminalWidth; } static Terminal createSystemTerminal() throws IOException { // Create a system Terminal. JNA is not shipped in the SDK JLINE3 jar. return TerminalBuilder.builder().jansi(Launcher.OS.getCurrent() == Launcher.OS.Windows).jna(false).system(true).signalHandler(Terminal.SignalHandler.SIG_IGN).build(); } private void printOption(String option, String description, int indentStart, int optionWidth) { String indent = spaces(indentStart); String desc = description != null ? description : ""; String nl = System.lineSeparator(); String[] descLines = desc.split(nl); for (int i = 0; i < descLines.length; i++) { descLines[i] = wrap(descLines[i], indent + spaces(optionWidth)); } if (option.length() >= optionWidth && description != null) { out.println(indent + option + nl + indent + spaces(optionWidth) + descLines[0]); } else { out.println(indent + option + spaces(optionWidth - option.length()) + descLines[0]); } for (int i = 1; i < descLines.length; i++) { out.println(indent + spaces(optionWidth) + descLines[i]); } } void printOption(PrintableOption option, int indentation) { printOption(option.option, option.description, indentation, optionIndent); } static final class PrintableOption implements Comparable { final String name; final String option; final String description; final boolean experimental; protected PrintableOption(String name, String option, String description, boolean experimental) { this.name = name; this.option = option; this.description = description; this.experimental = experimental; } @Override public int compareTo(PrintableOption o) { return this.name.compareTo(o.name); } } void printOptions(List options, String title, int indentation) { final List stableOptions = new ArrayList<>(); final List experimentalOptions = new ArrayList<>(); for (PrintableOption option : options) { if (option.experimental) { experimentalOptions.add(option); } else { stableOptions.add(option); } } out.println(spaces(indentation) + title); if (!stableOptions.isEmpty()) { out.println(spaces(indentation + 1) + "[Stable]"); Collections.sort(stableOptions); for (PrintableOption option : stableOptions) { printOption(option, indentation + 2); } } if (!experimentalOptions.isEmpty()) { out.println(spaces(indentation + 1) + "[Experimental]"); Collections.sort(experimentalOptions); for (PrintableOption option : experimentalOptions) { printOption(option, indentation + 2); } } } protected enum OS { Darwin, Linux, Solaris, Windows; private static OS findCurrent() { final String name = System.getProperty("os.name"); if (name.equals("Linux")) { return Linux; } if (name.equals("SunOS")) { return Solaris; } if (name.equals("Mac OS X") || name.equals("Darwin")) { return Darwin; } if (name.startsWith("Windows")) { return Windows; } throw new IllegalArgumentException("unknown OS: " + name); } private static final OS current = findCurrent(); public static OS getCurrent() { return current; } } /** * Prints a single line to the output stream, terminated with newline. * * @param l line text. * @since 20.0 */ protected final void println(String l) { out.println(l); } /** * Prints sequence of lines to the output stream. Each argument will be printed as a whole line, * terminated by a newline. * * @param lines lines * @since 20.0 */ protected final void println(String... lines) { for (String l : lines) { out.println(l); } } private void printJvmHelp() { println("JVM options:"); String classpathHelp = "Manage the classpath for Java libraries that you can access from guest languages ('" + File.pathSeparator + "' separated list)"; launcherOption("--vm.classpath=[" + File.pathSeparator + "path...]", classpathHelp); launcherOption("--vm.cp=[" + File.pathSeparator + "path...]", classpathHelp); launcherOption("--vm.D=", "Set a system property"); launcherOption("--vm.esa", "Enable system assertions"); launcherOption("--vm.ea[:...|:]", "Enable assertions with specified granularity"); launcherOption("--vm.agentlib:[=]", "Load native agent library "); launcherOption("--vm.agentpath:[=]", "Load native agent library by full pathname"); launcherOption("--vm.javaagent:[=]", "Load Java programming language agent"); launcherOption("--vm.Xbootclasspath/a:[" + File.pathSeparator + "path...]", "Append classpath entries to the JVM's boot classpath ('" + File.pathSeparator + "' separated list)"); launcherOption("--vm.Xmx", "Set maximum Java heap size"); launcherOption("--vm.Xms", "Set initial Java heap size"); launcherOption("--vm.Xss", "Set java thread stack size"); } private void printBasicNativeHelp() { launcherOption("--vm.D=", "Sets a system property"); launcherOption("--vm.Xmn", "Sets the maximum size of the young generation, in bytes."); launcherOption("--vm.Xmx", "Sets the maximum size of the heap, in bytes."); launcherOption("--vm.Xms", "Sets the minimum size of the heap, in bytes."); launcherOption("--vm.Xss", "Sets the size of each thread stack, in bytes."); } private static final String CLASSPATH = System.getProperty("org.graalvm.launcher.classpath"); /** * Returns what is an equivalent of {@code argv[0]}}, i.e., the command that executed the * launcher. If the launcher was executed via a symlink, this returns the symlink. If the * launcher was executed via a command resolved via the user's PATH environment variable, this * returns that command. * * @return The program name or {@code null} if not available. */ protected static String getProgramName() { // For thin launchers this system property should always be available in both JVM and native // mode. Bash launchers set this property explicitly. String executableName = System.getProperty(EXECUTABLE_NAME_PROPERTY); if (executableName != null) { return executableName; } // What remains are non-thin native launchers if (ImageInfo.inImageRuntimeCode()) { if (ProcessProperties.getArgumentVectorBlockSize() > 0) { return ProcessProperties.getArgumentVectorProgramName(); } } return null; } @SuppressWarnings("unused") @Deprecated(since = "20.3") protected final void maybeNativeExec(List args, boolean isPolyglotLauncher, Map polyglotOptions) { maybeNativeExec(args, args, isPolyglotLauncher); } /** * Possibly re-executes the launcher when JVM or polyglot mode is requested; call only if * {@link #isAOT()} is true. If the result is to run native, then it applies VM options on the * current process. * * The method parses the {@code unrecognizedArgs} for --jvm/--native/--polyglot flags and --vm.* * options. If JVM mode is requested, it execs a Java process configured with supported JVM * parameters and system properties over this process - in this case, the method does not return * (except errors). * * @param originalArgs the original arguments from main(), unmodified. * @param unrecognizedArgs a subset of {@code originalArgs} that was not recognized by * {@link AbstractLanguageLauncher#preprocessArguments(List, Map)}. All arguments * recognized by maybeExec are removed from the list. * @param isPolyglotLauncher whether this is the {@link PolyglotLauncher} (bin/polyglot) * @since 20.0 */ protected final void maybeNativeExec(List originalArgs, List unrecognizedArgs, boolean isPolyglotLauncher) { if (!IS_AOT) { return; } maybeExec(originalArgs, unrecognizedArgs, isPolyglotLauncher, getDefaultVMType(), false); } void maybeExec(List originalArgs, List unrecognizedArgs, boolean isPolyglotLauncher, VMType defaultVmType, boolean thinLauncher) { assert isAOT(); VMType vmType = null; boolean polyglot = false; List jvmArgs = new ArrayList<>(); List applicationArgs = new ArrayList<>(originalArgs); Iterator iterator = unrecognizedArgs.iterator(); List vmOptions = new ArrayList<>(); while (iterator.hasNext()) { String arg = iterator.next(); if (arg.equals("--jvm")) { if (vmType == VMType.Native) { throw abort("'--jvm' and '--native' options can not be used together."); } if (isStandalone()) { throw abort("'--jvm' is only supported when this launcher is part of a GraalVM."); } vmType = VMType.JVM; iterator.remove(); } else if (arg.equals("--native")) { if (vmType == VMType.JVM) { throw abort("'--jvm' and '--native' options can not be used together."); } vmType = VMType.Native; iterator.remove(); } else if (arg.startsWith("--vm.") && arg.length() > "--vm.".length()) { String vmArg = arg.substring("--vm.".length()); if (vmArg.equals("classpath")) { throw abort("'--vm.classpath' argument must be of the form '--vm.classpath=', not two separate arguments"); } if (vmArg.equals("cp")) { throw abort("'--vm.cp' argument must be of the form '--vm.cp=', not two separate arguments"); } if (vmArg.startsWith("classpath=") || vmArg.startsWith("cp=")) { int eqIndex = vmArg.indexOf('='); jvmArgs.add('-' + vmArg.substring(0, eqIndex)); jvmArgs.add(vmArg.substring(eqIndex + 1)); } else { vmOptions.add(vmArg); } iterator.remove(); } else if (arg.equals("--polyglot")) { polyglot = true; } } boolean isDefaultVMType = false; if (vmType == null) { vmType = defaultVmType; isDefaultVMType = true; } if (vmType == VMType.JVM) { for (String vmOption : vmOptions) { jvmArgs.add('-' + vmOption); } if (!isPolyglotLauncher && polyglot) { applicationArgs.add(0, "--polyglot"); } assert !isStandalone(); if (thinLauncher) { Map env = new HashMap<>(); env.put("GRAALVM_LAUNCHER_FORCE_JVM", "true"); env.put("GRAALVM_LAUNCHER_EXECUTABLE_NAME", System.getProperty(EXECUTABLE_NAME_PROPERTY)); nativeAccess.reExec(originalArgs, env); } else { executeJVM(nativeAccess == null ? System.getProperty("java.class.path") : nativeAccess.getClasspath(jvmArgs), jvmArgs, applicationArgs, Collections.emptyMap()); } } else { assert vmType == VMType.Native; /* * If the VM args have already been applied (e.g. by the thin launcher), there is no * need to set them again at runtime */ if (!thinLauncher) { for (String vmOption : vmOptions) { nativeAccess.setNativeOption(vmOption); } } /* * All options are processed, now we can run the startup hooks that can depend on the * option values. */ VMRuntime.initialize(); if (!isPolyglotLauncher && polyglot) { assert jvmArgs.isEmpty(); if (isStandalone()) { throw abort("--polyglot option is only supported when this launcher is part of a GraalVM."); } executePolyglot(applicationArgs, Collections.emptyMap(), !isDefaultVMType); } } } @SuppressWarnings("unused") @Deprecated(since = "20.3") protected void executeJVM(String classpath, List jvmArgs, List remainingArgs, Map polyglotOptions) { executeJVM(classpath, jvmArgs, remainingArgs); } /** * Called if a JVM has to be started instead of AOT binary. The method is only called in AOT * mode. Subclasses may override to apply different options or launch mechanism * * @param classpath class path to be used with the JVM * @param jvmArgs arguments for the VM * @param remainingArgs main arguments */ protected void executeJVM(String classpath, List jvmArgs, List remainingArgs) { nativeAccess.execJVM(classpath, jvmArgs, remainingArgs); } @SuppressWarnings("unused") @Deprecated(since = "20.3") protected void executePolyglot(List mainArgs, Map polyglotOptions, boolean forceNative) { executePolyglot(mainArgs, forceNative); } /** * Called to execute the bin/polyglot launcher with the supplied options. Subclasses may * eventually override and implement in a different way. * * @param mainArgs program arguments */ protected void executePolyglot(List mainArgs, boolean forceNative) { nativeAccess.executePolyglot(mainArgs, forceNative); } class Native { // execve() to JVM/polyglot from native if needed. // Only parses --jvm/--native to find the VMType and --vm.* to pass/set the VM options. private void setNativeOption(String arg) { if (arg.startsWith("Djdk.graal.")) { setGraalStyleRuntimeOption(arg.substring("Djdk.graal.".length())); } else if (arg.startsWith("D")) { setSystemProperty(arg.substring("D".length())); } else if (arg.startsWith("XX:")) { setRuntimeOption(arg.substring("XX:".length())); } else if (arg.startsWith("X")) { if (isXOption(arg)) { setXOption(arg.substring("X".length())); } else { throw abort("Unrecognized vm option: '--vm." + arg + "'. Some VM options may be only supported in --jvm mode."); } } else { throw abort("Unrecognized vm option: '--vm." + arg + "'. Such arguments should start with '--vm.D', '--vm.XX:', or '--vm.X'"); } } private void setGraalStyleRuntimeOption(String arg) { if (arg.startsWith("+") || arg.startsWith("-")) { throw abort("Dgraal option must use = format, not +/- prefix"); } int eqIdx = arg.indexOf('='); String key; String value; if (eqIdx < 0) { key = arg; value = ""; } else { key = arg.substring(0, eqIdx); value = arg.substring(eqIdx + 1); } RuntimeOptions.Descriptor descriptor = RuntimeOptions.getDescriptor(key); if (descriptor == null) { throw unknownOption(key); } try { RuntimeOptions.set(key, descriptor.convertValue(value)); } catch (IllegalArgumentException iae) { throw abort("Invalid argument: '--vm." + arg + "': " + iae.getMessage()); } } public void setSystemProperty(String arg) { int eqIdx = arg.indexOf('='); String key; String value; if (eqIdx < 0) { key = arg; value = ""; } else { key = arg.substring(0, eqIdx); value = arg.substring(eqIdx + 1); } System.setProperty(key, value); } public void setRuntimeOption(String arg) { int eqIdx = arg.indexOf('='); String key; Object value; if (arg.startsWith("+") || arg.startsWith("-")) { key = arg.substring(1); if (eqIdx >= 0) { throw abort("Invalid argument: '--vm." + arg + "': Use either +/- or =, but not both"); } RuntimeOptions.Descriptor descriptor = getVMOptionDescriptor(key); if (!isBooleanOption(descriptor)) { throw abort("Invalid argument: " + key + " is not a boolean option, set it with --vm.XX:" + key + "=."); } value = arg.startsWith("+"); } else if (eqIdx > 0) { key = arg.substring(0, eqIdx); RuntimeOptions.Descriptor descriptor = getVMOptionDescriptor(key); if (isBooleanOption(descriptor)) { throw abort("Boolean option '" + key + "' must be set with +/- prefix, not = format."); } try { value = descriptor.convertValue(arg.substring(eqIdx + 1)); } catch (IllegalArgumentException iae) { throw abort("Invalid argument: '--vm." + arg + "': " + iae.getMessage()); } } else { throw abort("Invalid argument: '--vm." + arg + "'. Prefix boolean options with + or -, suffix other options with ="); } RuntimeOptions.set(key, value); } private RuntimeOptions.Descriptor getVMOptionDescriptor(String key) { RuntimeOptions.Descriptor descriptor = RuntimeOptions.getDescriptor(key); if (descriptor == null) { throw unknownOption(key); } return descriptor; } /* Is an option that starts with an 'X' one of the recognized X options? */ private boolean isXOption(String arg) { return (arg.startsWith("Xmn") || arg.startsWith("Xms") || arg.startsWith("Xmx") || arg.startsWith("Xss")); } /* Set a `-X` option, given something like "mx2g". */ private void setXOption(String arg) { try { RuntimeOptions.set(arg, null); } catch (RuntimeException re) { throw abort("Invalid argument: '--vm.X" + arg + "' does not specify a valid number."); } } private boolean isBooleanOption(RuntimeOptions.Descriptor descriptor) { return descriptor.valueType() == Boolean.class; } private AbortException unknownOption(String key) { throw abort("Unknown native option: " + key + ". Use --help:vm to list available options."); } private void printNativeHelp() { System.out.println("Native VM options:"); SortedMap sortedOptions = new TreeMap<>(); for (RuntimeOptions.Descriptor descriptor : RuntimeOptions.listDescriptors()) { if (!descriptor.deprecated()) { sortedOptions.put(descriptor.name(), descriptor); } } for (Entry entry : sortedOptions.entrySet()) { RuntimeOptions.Descriptor descriptor = entry.getValue(); String helpMsg = descriptor.help(); if (isBooleanOption(descriptor)) { Boolean val = (Boolean) descriptor.defaultValue(); if (helpMsg.length() != 0) { helpMsg += ' '; } if (val == null || !val) { helpMsg += "Default: - (disabled)."; } else { helpMsg += "Default: + (enabled)."; } launcherOption("--vm.XX:\u00b1" + entry.getKey(), helpMsg); } else { Object def = descriptor.defaultValue(); if (def instanceof String) { def = "\"" + def + "\""; } launcherOption("--vm.XX:" + entry.getKey() + "=" + def, helpMsg); } } printBasicNativeHelp(); } private void executePolyglot(List args, boolean forceNative) { List command = new ArrayList<>(args.size() + 3); Path executable = getGraalVMBinaryPath("polyglot"); if (forceNative) { command.add("--native"); } command.add("--use-launcher"); command.add(getMainClass()); command.addAll(args); exec(executable, command); } private void execJVM(String classpath, List jvmArgs, List args) { // TODO use String[] for command to avoid a copy later List command = new ArrayList<>(jvmArgs.size() + args.size() + 4); Path executable = getGraalVMBinaryPath("java"); if (classpath != null) { command.add("-classpath"); command.add(classpath); } command.addAll(jvmArgs); command.add(getMainClass()); command.addAll(args); exec(executable, command); } private String getClasspath(List jvmArgs) { assert isAOT(); assert CLASSPATH != null; StringBuilder sb = new StringBuilder(); if (!CLASSPATH.isEmpty()) { Path graalVMHome = getGraalVMHome(); if (graalVMHome == null) { throw abort("Can not resolve classpath: could not get GraalVM home"); } for (String entry : CLASSPATH.split(File.pathSeparator)) { // On Windows, Path.resolve will throw an error on * character in path. boolean endsWithStar = entry.endsWith("*"); Path resolved; if (endsWithStar) { resolved = graalVMHome.resolve(entry.substring(0, entry.length() - 1)); } else { resolved = graalVMHome.resolve(entry); } if (isVerbose() && !Files.exists(resolved)) { warn("%s does not exist", resolved); } sb.append(resolved); if (endsWithStar) { if (!resolved.endsWith(File.separator)) { sb.append(File.separator); } sb.append("*"); } sb.append(File.pathSeparatorChar); } } String classpathFromArgs = null; Iterator iterator = jvmArgs.iterator(); while (iterator.hasNext()) { String jvmArg = iterator.next(); if (jvmArg.equals("-cp") || jvmArg.equals("-classpath")) { if (iterator.hasNext()) { iterator.remove(); classpathFromArgs = iterator.next(); iterator.remove(); // no break, pick the last one } } if (jvmArg.startsWith("-Djava.class.path=")) { iterator.remove(); classpathFromArgs = jvmArg.substring("-Djava.class.path=".length()); } } if (classpathFromArgs != null) { sb.append(classpathFromArgs); sb.append(File.pathSeparatorChar); } if (sb.length() == 0) { return null; } return sb.substring(0, sb.length() - 1); } /** * Re-rexecutes the launcher executable with the given arguments and additional environment. * * @param args launcher arguments * @param env additional environment - the entries will be added to the existing environment */ private void reExec(List args, Map env) { assert isAOT(); String path = ProcessProperties.getExecutableName(); Path executable = Paths.get(path); if (isVerbose()) { StringBuilder sb = formatExec(executable, args); err.print(sb.toString()); } Map newEnv = new HashMap<>(); newEnv.putAll(System.getenv()); newEnv.putAll(env); // for exec, arg 0 needs to be the name of the executable List execArgs = new ArrayList<>(); execArgs.add(path); execArgs.addAll(args); ProcessProperties.exec(executable, execArgs.toArray(new String[0]), newEnv); } private void exec(Path executable, List command) { assert isAOT(); if (isVerbose()) { StringBuilder sb = formatExec(executable, command); err.print(sb.toString()); } String[] argv = new String[command.size() + 1]; int i = 0; Path filename = executable.getFileName(); if (filename == null) { throw abort(String.format("Cannot determine execute filename from path %s", filename)); } argv[i++] = filename.toString(); for (String arg : command) { argv[i++] = arg; } ProcessProperties.exec(executable, argv); } private StringBuilder formatExec(Path executable, List command) { StringBuilder sb = new StringBuilder("exec: "); sb.append(executable); for (String arg : command) { sb.append(' '); sb.append(ShellQuotes.quote(arg)); } sb.append(System.lineSeparator()); return sb; } } private static final class ShellQuotes { private static final BitSet safeChars; static { safeChars = new BitSet(); safeChars.set('a', 'z' + 1); safeChars.set('A', 'Z' + 1); safeChars.set('+', ':' + 1); // +,-./0..9: safeChars.set('@'); safeChars.set('%'); safeChars.set('_'); safeChars.set('='); } private static String quote(String str) { if (str.isEmpty()) { return "''"; } for (int i = 0; i < str.length(); i++) { if (!safeChars.get(str.charAt(i))) { return "'" + str.replace("'", "'\"'\"'") + "'"; } } return str; } } /** * Creates a new log file. The method uses a supplemental lock file to determine the file is * still opened for output; in that case, it creates a different file, named `path'1, `path`2, * ... until it finds a free name. Files not locked (actively written to) are overwritten. * * @param path the desired output for log * @return the OutputStream for logging * @throws IOException in case of I/O error opening the file * @since 20.0 */ protected static OutputStream newLogStream(Path path) throws IOException { Path usedPath = path; Path fileNamePath = path.getFileName(); String fileName = fileNamePath == null ? "" : fileNamePath.toString(); OutputStream outputStream; if (Files.exists(path) && !Files.isRegularFile(path)) { // Don't try to lock device or named pipe. outputStream = new BufferedOutputStream(Files.newOutputStream(usedPath, WRITE, CREATE, APPEND)); } else { Path lockFile = null; FileChannel lockFileChannel = null; for (int unique = 0;; unique++) { StringBuilder lockFileNameBuilder = new StringBuilder(fileName); if (unique > 0) { lockFileNameBuilder.append(unique); usedPath = path.resolveSibling(lockFileNameBuilder.toString()); } lockFileNameBuilder.append(".lck"); lockFile = path.resolveSibling(lockFileNameBuilder.toString()); Pair openResult = openChannel(lockFile); if (openResult != null) { lockFileChannel = openResult.getLeft(); if (lock(lockFileChannel, openResult.getRight())) { break; } else { // Close and try next name lockFileChannel.close(); } } } assert lockFile != null && lockFileChannel != null; boolean success = false; try { outputStream = new LockableOutputStream( new BufferedOutputStream(Files.newOutputStream(usedPath, WRITE, CREATE, APPEND)), lockFile, lockFileChannel); success = true; } finally { if (!success) { LockableOutputStream.unlock(lockFile, lockFileChannel); } } } return outputStream; } private static Pair openChannel(Path path) throws IOException { FileChannel channel = null; for (int retries = 0; channel == null && retries < 2; retries++) { try { channel = FileChannel.open(path, CREATE_NEW, WRITE); return Pair.create(channel, true); } catch (FileAlreadyExistsException faee) { // Maybe a FS race showing a zombie file, try to reuse it if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) && isParentWritable(path)) { try { channel = FileChannel.open(path, WRITE, APPEND); return Pair.create(channel, false); } catch (NoSuchFileException x) { // FS Race, next try we should be able to create with CREATE_NEW } catch (IOException x) { return null; } } else { return null; } } } return null; } private static boolean isParentWritable(Path path) { Path parentPath = path.getParent(); if (parentPath == null && !path.isAbsolute()) { parentPath = path.toAbsolutePath().getParent(); } return parentPath != null && Files.isWritable(parentPath); } private static boolean lock(FileChannel lockFileChannel, boolean newFile) { boolean available = false; try { available = lockFileChannel.tryLock() != null; } catch (OverlappingFileLockException ofle) { // VM already holds lock continue with available set to false } catch (IOException ioe) { // Locking not supported by OS available = newFile; } return available; } private static final class LockableOutputStream extends OutputStream { private final OutputStream delegate; private final Path lockFile; private final FileChannel lockFileChannel; LockableOutputStream(OutputStream delegate, Path lockFile, FileChannel lockFileChannel) { this.delegate = delegate; this.lockFile = lockFile; this.lockFileChannel = lockFileChannel; } @Override public void write(int b) throws IOException { delegate.write(b); } @Override public void write(byte[] b) throws IOException { delegate.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { delegate.write(b, off, len); } @Override public void flush() throws IOException { delegate.flush(); } @Override public void close() throws IOException { try { delegate.close(); } finally { unlock(lockFile, lockFileChannel); } } private static void unlock(Path lockFile, FileChannel lockFileChannel) { try { lockFileChannel.close(); } catch (IOException ioe) { // Error while closing the channel, ignore. } try { Files.delete(lockFile); } catch (IOException ioe) { // Error while deleting the lock file, ignore. } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy