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

io.helidon.linker.ClassDataSharing Maven / Gradle / Ivy

/*
 * Copyright (c) 2019, 2021 Oracle and/or its affiliates.
 *
 * 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 io.helidon.linker;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.helidon.build.util.FileUtils;
import io.helidon.build.util.Log.Level;
import io.helidon.build.util.LogFormatter;
import io.helidon.build.util.OSType;
import io.helidon.build.util.PrintStreams;
import io.helidon.build.util.ProcessMonitor;
import io.helidon.linker.util.Constants;
import io.helidon.linker.util.JavaRuntime;

import static io.helidon.build.util.FileUtils.assertDir;
import static io.helidon.build.util.FileUtils.assertFile;
import static io.helidon.build.util.FileUtils.fileName;
import static io.helidon.build.util.PrintStreams.DEVNULL;
import static io.helidon.build.util.PrintStreams.STDERR;
import static io.helidon.build.util.PrintStreams.STDOUT;
import static io.helidon.linker.Configuration.Builder.DEFAULT_MAX_APP_START_SECONDS;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;

/**
 * A builder for a CDS archive for a Helidon application either as a jar or a module.
 * Assumes that it can cause the application to exit once startup has completed by setting the "exit.on.startup" system property.
 */
public final class ClassDataSharing {
    private final Path applicationJar;
    private final String applicationModule;
    private final Path jri;
    private final Path classListFile;
    private final Path archiveFile;
    private final List classList;

    /**
     * Returns a new {@link Builder}.
     *
     * @return The builder.
     */
    public static Builder builder() {
        return new Builder();
    }

    private ClassDataSharing(Builder builder) {
        this.applicationJar = builder.mainJar;
        this.applicationModule = builder.applicationModule;
        this.jri = builder.jri;
        this.classListFile = builder.classListFile;
        this.archiveFile = builder.archiveFile;
        this.classList = builder.classList;
    }

    /**
     * Returns the path to the main application jar.
     *
     * @return The path. Will be {@code null} if a moduleName was used.
     */
    public Path applicationJar() {
        return applicationJar;
    }

    /**
     * Returns the name of the main application module.
     *
     * @return The name. Will be {@code null} if a jar was used.
     */
    public String applicationModule() {
        return applicationModule;
    }

    /**
     * Returns the path to the JRI used to build the archive.
     *
     * @return The path.
     */
    public Path jri() {
        return jri;
    }

    /**
     * Returns the path to the list of classes collected during application startup.
     *
     * @return The path.
     */
    public Path classListFile() {
        return classListFile;
    }

    /**
     * Returns the classes collected during application startup.
     *
     * @return The class names.
     */
    public List classList() {
        return classList;
    }

    /**
     * Returns the path to the archive.
     *
     * @return The path.
     */
    public Path archiveFile() {
        return archiveFile;
    }

    /**
     * Builder.
     */
    public static final class Builder {
        private static final String FILE_PREFIX = "start";
        private static final String ARCHIVE_NAME = FILE_PREFIX + ".jsa";
        private static final String CLASS_LIST_FILE_SUFFIX = ".classlist";
        private static final String JAR_SUFFIX = ".jar";
        private static final String XSHARE_OFF = "-Xshare:off";
        private static final String XSHARE_DUMP = "-Xshare:dump";
        private static final String XX_DUMP_LOADED_CLASS_LIST = "-XX:DumpLoadedClassList=";
        private static final String XX_SHARED_ARCHIVE_FILE = "-XX:SharedArchiveFile=";
        private static final String XX_SHARED_CLASS_LIST_FILE = "-XX:SharedClassListFile=";
        private static final String EXIT_ON_STARTED = "-Dexit.on.started=";
        private static final String EXIT_ON_STARTED_VALUE = "!";
        private static final String UTF_8_ENCODING = "-Dfile.encoding=UTF-8";
        private static final String SKIPPED_CLASS_PREFIX = "skip writing class";
        private static final String CANNOT_FIND_PREFIX = "Preload Warning: Cannot find";
        private static final String LIB_DIR_NAME = "lib";
        private Path jri;
        private String archiveDir;
        private String applicationModule;
        private Path mainJar;
        private Path classListFile;
        private Path archiveFile;
        private List classList;
        private boolean createArchive;
        private String target;
        private String targetOption;
        private String targetDescription;
        private boolean logOutput;
        private List jvmOptions;
        private List args;
        private String exitOnStartedValue;
        private int maxWaitSeconds;

        private Builder() {
            this.createArchive = true;
            this.archiveDir = LIB_DIR_NAME;
            this.jvmOptions = emptyList();
            this.args = emptyList();
            this.exitOnStartedValue = EXIT_ON_STARTED_VALUE;
            this.maxWaitSeconds = DEFAULT_MAX_APP_START_SECONDS;
        }

        /**
         * Sets the path to the JRI to use when building the archive.
         *
         * @param jri The path.
         * @return The builder.
         */
        public Builder jri(Path jri) {
            this.jri = jri;
            javaPath(); // Validate
            return this;
        }

        /**
         * Sets the path to the main application jar.
         *
         * @param mainJar The path to the main application jar.
         * @return The builder.
         */
        public Builder applicationJar(Path mainJar) {
            if (requireNonNull(mainJar).isAbsolute()) {
                this.mainJar = assertJar(mainJar);
            } else {
                this.mainJar = assertJar(jri.resolve(mainJar));
            }
            return this;
        }

        /**
         * Sets the name of the main application module.
         *
         * @param mainModuleName The the name of the main application module.
         * @return The builder.
         */
        public Builder applicationModule(String mainModuleName) {
            this.applicationModule = requireNonNull(mainModuleName);
            return this;
        }

        /**
         * Sets JVM options to use when starting the application.
         *
         * @param jvmOptions The options.
         * @return The builder.
         */
        public Builder jvmOptions(List jvmOptions) {
            if (isValid(jvmOptions)) {
                this.jvmOptions = jvmOptions;
            }
            return this;
        }

        /**
         * Sets arguments to use when starting the application.
         *
         * @param args The arguments.
         * @return The builder.
         */
        public Builder args(List args) {
            if (isValid(args)) {
                this.args = args;
            }
            return this;
        }

        /**
         * Sets the path of the CDS archive file to create.
         *
         * @param archiveFile The path.
         * @return The builder.
         */
        public Builder archiveFile(Path archiveFile) {
            this.archiveFile = requireNonNull(archiveFile);
            return this;
        }

        /**
         * Sets whether or not to create the CDS archive. Defaults to {@code true}.
         *
         * @param createArchive {@code true} if the archive should be created.
         * @return The builder.
         */
        public Builder createArchive(boolean createArchive) {
            this.createArchive = createArchive;
            return this;
        }

        /**
         * Sets whether or not to output from the build process(es) should be logged.
         * Defaults to {@code false} and will include the output in any exception message.
         *
         * @param logOutput {@code true} if output should be logged.
         * @return The builder.
         */
        public Builder logOutput(boolean logOutput) {
            this.logOutput = logOutput;
            return this;
        }

        /**
         * Sets the maximum number of seconds to wait for completion.
         *
         * @param maxWaitSeconds The number of seconds.
         * @return The builder.
         */
        public Builder maxWaitSeconds(int maxWaitSeconds) {
            this.maxWaitSeconds = maxWaitSeconds;
            return this;
        }

        /**
         * Sets the path of the class list file to use. One is generated if not provided.
         *
         * @param classListFile The path.
         * @return The builder.
         */
        public Builder classListFile(Path classListFile) {
            this.classListFile = assertFile(classListFile);
            return this;
        }

        /**
         * Sets the {@code -Dexit.on.started} property value.
         *
         * @param exitOnStartedValue The value
         * @return The builder.
         */
        public Builder exitOnStartedValue(String exitOnStartedValue) {
            this.exitOnStartedValue = requireNonNull(exitOnStartedValue);
            return this;
        }

        /**
         * Build the instance.
         *
         * @return The instance.
         * @throws Exception If an error occurs.
         */
        public ClassDataSharing build() throws Exception {
            requireNonNull(jri, "java home required");
            if (mainJar == null && applicationModule == null) {
                throw new IllegalStateException("Either application jar or module name required");
            } else if (mainJar != null && applicationModule != null) {
                throw new IllegalStateException("Cannot specify both application jar and module name");
            } else if (mainJar != null) {
                this.targetOption = "-jar";
                // Note that for CDS archives to work correctly, the path used at runtime must be the
                // same as that used here. Make this path relative to the JRI so that it can be moved
                // around and still function.
                this.target = jri.relativize(mainJar).toString();
                this.targetDescription = fileName(mainJar);
            } else {
                this.targetOption = "-m";
                this.target = applicationModule;
                this.targetDescription = "module " + target + " in " + jri;
            }

            if (classListFile == null) {
                this.classListFile = tempFile(CLASS_LIST_FILE_SUFFIX);
                this.classList = buildClassList();
            } else {
                this.classList = loadClassList();
            }

            if (createArchive) {
                if (archiveFile == null) {
                    archiveFile = assertDir(jri.resolve(archiveDir)).resolve(ARCHIVE_NAME);
                }
                buildCdsArchive();
            }

            return new ClassDataSharing(this);
        }

        private List buildClassList() throws Exception {
            execute("Creating startup class list for " + targetDescription,
                    XSHARE_OFF, XX_DUMP_LOADED_CLASS_LIST + classListFile, UTF_8_ENCODING);
            return loadClassList();
        }

        @SuppressWarnings("ResultOfMethodCallIgnored")
        private void buildCdsArchive() throws Exception {
            final String action = "Creating Class Data Sharing archive for " + targetDescription;
            if (Constants.CDS_REQUIRES_UNLOCK_OPTION) {
                execute(action, Constants.CDS_UNLOCK_OPTIONS, XSHARE_DUMP, XX_SHARED_ARCHIVE_FILE + archiveFile,
                        XX_SHARED_CLASS_LIST_FILE + classListFile, UTF_8_ENCODING);
            } else {
                execute(action, XSHARE_DUMP, XX_SHARED_ARCHIVE_FILE + archiveFile,
                        XX_SHARED_CLASS_LIST_FILE + classListFile, UTF_8_ENCODING);
            }
            if (Constants.OS == OSType.Windows) {
                // Try to make the archive file writable so that a second run can delete the image
                archiveFile.toFile().setWritable(true);
            }
        }

        private List loadClassList() throws IOException {
            return Files.readAllLines(classListFile);
        }

        private void execute(String action, String... jvmArgs) throws Exception {
            final ProcessBuilder processBuilder = new ProcessBuilder();
            final List command = new ArrayList<>();

            command.add(javaPath().toString());
            command.addAll(jvmOptions);
            command.add(EXIT_ON_STARTED + exitOnStartedValue);
            command.addAll(Arrays.asList(jvmArgs));
            command.add(targetOption);
            command.add(target);
            command.addAll(args);
            processBuilder.command(command);

            processBuilder.directory(jri.toFile());

            PrintStream stdOut;
            PrintStream stdErr;
            if (logOutput) {
                stdOut = PrintStreams.apply(STDOUT, LogFormatter.of(Level.DEBUG));
                stdErr = PrintStreams.apply(STDERR, LogFormatter.of(Level.WARN));
            } else {
                stdOut = DEVNULL;
                stdErr = DEVNULL;
            }
            ProcessMonitor.builder()
                          .description(action)
                          .processBuilder(processBuilder)
                          .stdOut(stdOut)
                          .stdErr(stdErr)
                          .filter(Builder::filter)
                          .build()
                          .execute(maxWaitSeconds, TimeUnit.SECONDS);
        }

        private static boolean filter(String line) {
            return !line.startsWith(SKIPPED_CLASS_PREFIX) && !line.startsWith(CANNOT_FIND_PREFIX);
        }

        private Path javaPath() {
            return JavaRuntime.javaCommand(jri);
        }

        private static boolean isValid(Collection value) {
            return value != null && !value.isEmpty();
        }

        private static Path tempFile(String suffix) throws IOException {
            final File file = File.createTempFile(FILE_PREFIX, suffix);
            file.deleteOnExit();
            return file.toPath();
        }

        private static Path assertJar(Path path) {
            final String fileName = FileUtils.fileName(assertFile(path));
            if (!fileName.endsWith(JAR_SUFFIX)) {
                throw new IllegalArgumentException(path + " is not a jar");
            }
            return path;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy