
io.helidon.linker.StartScript Maven / Gradle / Ivy
/*
* Copyright (c) 2019, 2020 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.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.IntStream;
import io.helidon.build.util.FileUtils;
import io.helidon.build.util.Log;
import io.helidon.build.util.OSType;
import io.helidon.build.util.ProcessMonitor;
import io.helidon.build.util.StreamUtils;
import static io.helidon.build.util.FileUtils.assertDir;
import static io.helidon.build.util.OSType.Unknown;
import static io.helidon.linker.util.Constants.CDS_REQUIRES_UNLOCK_OPTION;
import static io.helidon.linker.util.Constants.CDS_SUPPORTS_IMAGE_COPY;
import static io.helidon.linker.util.Constants.CDS_UNLOCK_OPTIONS;
import static io.helidon.linker.util.Constants.DIR_SEP;
import static io.helidon.linker.util.Constants.EOL;
import static io.helidon.linker.util.Constants.OS;
import static io.helidon.linker.util.Constants.WINDOWS_SCRIPT_EXECUTION_ERROR;
import static io.helidon.linker.util.Constants.WINDOWS_SCRIPT_EXECUTION_POLICY_ERROR;
import static io.helidon.linker.util.Constants.WINDOWS_SCRIPT_EXECUTION_POLICY_ERROR_HELP;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
/**
* Installs a start script for a main jar.
*/
public class StartScript {
/**
* The script file name.
*/
public static final String SCRIPT_FILE_NAME = OS.withScriptExtension("start");
private final Path installDirectory;
private final Path scriptFile;
private final String script;
private final int maxAppStartSeconds;
/**
* Returns a new builder.
*
* @return The builder.
*/
static Builder builder() {
return new Builder();
}
private StartScript(Builder builder) {
this.installDirectory = builder.scriptInstallDirectory;
this.scriptFile = builder.scriptFile;
this.script = builder.script;
this.maxAppStartSeconds = builder.maxAppStartSeconds;
}
/**
* Returns the install directory.
*
* @return The directory.
*/
Path installDirectory() {
return installDirectory;
}
/**
* Install the script.
*
* @return The path to the installed script.
*/
Path install() {
try {
Files.copy(new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8)), scriptFile);
if (OS.isPosix()) {
Files.setPosixFilePermissions(scriptFile, Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_EXECUTE
));
}
return scriptFile;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Execute the script with the given arguments.
*
* @param transform the output transform.
* @param args The arguments.
* @throws RuntimeException If the process fails.
*/
public void execute(Function transform, String... args) {
final ProcessBuilder processBuilder = new ProcessBuilder();
final List command = new ArrayList<>();
final Path root = requireNonNull(requireNonNull(scriptFile.getParent()).getParent());
if (OS.scriptExecutor() != null) {
command.add(OS.scriptExecutor());
}
command.add(scriptFile.toString());
command.addAll(Arrays.asList(args));
Log.debug("Commands: %s", command.toString());
processBuilder.command(command);
processBuilder.directory(root.toFile());
final ProcessMonitor monitor = ProcessMonitor.builder()
.processBuilder(processBuilder)
.stdOut(Log::info)
.stdErr(Log::warn)
.transform(transform)
.capture(true)
.build();
try {
monitor.execute(maxAppStartSeconds, TimeUnit.SECONDS);
checkWindowsExecutionPolicyError(monitor, false);
} catch (ProcessMonitor.ProcessFailedException e) {
checkWindowsExecutionPolicyError(e.monitor(), true);
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void checkWindowsExecutionPolicyError(ProcessMonitor monitor, boolean failed) {
if (OS == OSType.Windows) {
// We might have silently failed (but with warnings), and we have to deal with output that
// is split across lines, so join stderr output
final String stdErr = String.join(" ", monitor.stdErr());
if (failed || stdErr.contains(WINDOWS_SCRIPT_EXECUTION_ERROR)) {
final StringBuilder msg = new StringBuilder();
msg.append("Generated ").append(scriptFile.getFileName()).append(" script failed.");
// Add help message if this is the execution policy error
if (containsAll(stdErr, WINDOWS_SCRIPT_EXECUTION_POLICY_ERROR)) {
msg.append(WINDOWS_SCRIPT_EXECUTION_POLICY_ERROR_HELP);
}
throw new RuntimeException(msg.toString());
}
}
}
/**
* Returns the script.
*
* @return The script.
*/
public String script() {
return script;
}
/**
* Returns the path to the script file.
*
* @return The path.
*/
public Path scriptFile() {
return scriptFile;
}
/**
* Returns the script.
*
* @return The script.
*/
@Override
public String toString() {
return script;
}
private static boolean containsAll(String message, List words) {
for (final String word : words) {
if (!message.contains(word)) {
return false;
}
}
return true;
}
/**
* Platform not supported error.
*/
public static final class PlatformNotSupportedError extends IllegalStateException {
private final List command;
private PlatformNotSupportedError(List command) {
this.command = command;
}
/**
* Returns the Java command to execute on this platform.
*
* @return The command.
*/
List command() {
return command;
}
}
/**
* Template configuration.
*/
public interface TemplateConfig {
/**
* Returns the path to the installation home directory.
*
* @return The path.
*/
Path installHomeDirectory();
/**
* Returns the path to the script install directory.
*
* @return The path.
*/
Path scriptInstallDirectory();
/**
* Returns the path to the main jar.
*
* @return The path.
*/
Path mainJar();
/**
* Returns the default JVM options.
*
* @return The options.
*/
List defaultJvmOptions();
/**
* Returns the default debug options.
*
* @return The options.
*/
List defaultDebugOptions();
/**
* Returns the default arguments.
*
* @return The arguments.
*/
List defaultArgs();
/**
* Returns whether or not CDS is installed.
*
* @return {@code true} if installed.
*/
boolean cdsInstalled();
/**
* Returns whether or not debug support is installed.
*
* @return {@code true} if installed.
*/
boolean debugInstalled();
/**
* Returns the {@code -Dexit.on.started} property value.
*
* @return The value.
*/
String exitOnStartedValue();
/**
* Returns whether or not CDS requires the unlock option.
*
* @return {@code true} if required.
*/
default boolean cdsRequiresUnlock() {
return cdsInstalled() && CDS_REQUIRES_UNLOCK_OPTION;
}
/**
* Returns whether or not CDS supports copying the image.
*
* @return {@code true} if supported.
*/
default boolean cdsSupportsImageCopy() {
return cdsInstalled() && CDS_SUPPORTS_IMAGE_COPY;
}
/**
* Returns the configuration as a command. Intended as a simple substitute for those
* cases where a template cannot be created.
*
* @return The command.
*/
default List toCommand() {
final List command = new ArrayList<>();
command.add("bin" + DIR_SEP + "java");
if (cdsInstalled()) {
if (cdsRequiresUnlock()) {
command.add(CDS_UNLOCK_OPTIONS);
}
command.add("-XX:SharedArchiveFile=lib" + DIR_SEP + "start.jsa");
command.add("-Xshare:auto");
}
command.addAll(defaultJvmOptions());
command.add("-jar");
command.add("app" + DIR_SEP + mainJar().getFileName());
command.addAll(defaultArgs());
return command;
}
}
/**
* Template renderer.
*/
public interface Template {
/**
* Returns the final text rendered using the given configuration.
*
* @param config The configuration.
* @return The rendered text.
*/
String render(TemplateConfig config);
}
/**
* A {@link Template} that relies on hand-coded modifications rather than on a full-fledged template engine.
* This approach supports having a template file be a valid script that can be error checked in an IDE.
*/
public abstract static class SimpleTemplate implements Template {
private final List template;
/**
* Constructor that loads the template from the given resource path.
*
* @param templateResourcePath The template.
*/
protected SimpleTemplate(String templateResourcePath) {
this(load(templateResourcePath));
}
/**
* Constructor.
*
* @param template The template lines.
*/
protected SimpleTemplate(List template) {
this.template = template;
}
/**
* Removes any lines that contain the given substring.
*
* @param substring The substring.
* @param ignoreCase {@code true} if substring match should ignore case.
*/
protected void removeLines(String substring, boolean ignoreCase) {
removeLines((index, line) -> contains(line, substring, ignoreCase));
}
/**
* Removes any lines that match the given predicate.
*
* @param predicate The predicate.
*/
protected void removeLines(BiPredicate predicate) {
for (int i = template.size() - 1; i >= 0; i--) {
if (predicate.test(i, template.get(i))) {
template.remove(i);
}
}
}
/**
* Returns the index of the first line that contains the given substring.
*
* @param startIndex The start index.
* @param substring The substring.
* @param ignoreCase {@code true} if substring match should ignore case.
* @return The index.
* @throws IllegalStateException if no matching line is found.
*/
protected int indexOf(int startIndex, String substring, boolean ignoreCase) {
return indexOf(startIndex, (index, line) -> contains(line, substring, ignoreCase));
}
/**
* Returns the index of the first line that is equals to the given str.
*
* @param startIndex The start index.
* @param str The string.
* @return The index.
* @throws IllegalStateException if no matching line is found.
*/
protected int indexOfEquals(int startIndex, String str) {
return indexOf(startIndex, (index, line) -> line.equals(str));
}
/**
* Returns the index of the first line that matches the given predicate.
*
* @param startIndex The start index.
* @param predicate The predicate.
* @return The index.
* @throws IllegalStateException if no matching line is found.
*/
protected int indexOf(int startIndex, BiPredicate predicate) {
return IntStream.range(startIndex, template.size())
.filter(index -> predicate.test(index, template.get(index)))
.findFirst()
.orElseThrow(IllegalStateException::new);
}
/**
* Replaces the given substring in each line.
*
* @param substring The substring.
* @param replacement The replacement.
*/
protected void replace(String substring, String replacement) {
for (int i = 0; i < template.size(); i++) {
template.set(i, template.get(i).replace(substring, replacement));
}
}
/**
* Returns the last modified time of the given file, in seconds.
*
* @param file The file.
* @return The last modified time.
*/
protected static String lastModifiedTime(Path file) {
return Long.toString(FileUtils.lastModifiedSeconds(file));
}
@Override
public String toString() {
return String.join(EOL, template);
}
private static boolean contains(String line, String substring, boolean ignoreCase) {
return ignoreCase ? line.toLowerCase().contains(substring) : line.contains(substring);
}
private static List load(String resourcePath) {
final InputStream content = SimpleTemplate.class.getClassLoader().getResourceAsStream(resourcePath);
if (content == null) {
throw new IllegalStateException(resourcePath + " not found");
} else {
try {
return StreamUtils.toLines(content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}
/**
* The builder.
*/
public static final class Builder {
private Path installHomeDirectory;
private Path scriptInstallDirectory;
private Path mainJar;
private List defaultJvmOptions;
private List defaultDebugOptions;
private List defaultArgs;
private boolean cdsInstalled;
private boolean debugInstalled;
private String exitOnStartedValue;
private Template template;
private TemplateConfig config;
private Path scriptFile;
private String script;
private int maxAppStartSeconds;
private Builder() {
this.defaultJvmOptions = emptyList();
this.defaultDebugOptions = List.of(Configuration.Builder.DEFAULT_DEBUG);
this.cdsInstalled = true;
this.debugInstalled = true;
this.exitOnStartedValue = "!";
this.defaultArgs = emptyList();
this.maxAppStartSeconds = Configuration.Builder.DEFAULT_MAX_APP_START_SECONDS;
}
/**
* Sets the install home directory.
*
* @param installHomeDirectory The target.
* @return The builder.
*/
public Builder installHomeDirectory(Path installHomeDirectory) {
this.installHomeDirectory = assertDir(installHomeDirectory);
this.scriptInstallDirectory = assertDir(installHomeDirectory).resolve("bin");
return this;
}
/**
* Sets the path to the main jar.
*
* @param mainJar The path. May not be {@code null}.
* @return The builder.
*/
public Builder mainJar(Path mainJar) {
this.mainJar = FileUtils.assertFile(mainJar);
return this;
}
/**
* Sets the default JVM options.
*
* @param jvmOptions The options.
* @return The builder.
*/
public Builder defaultJvmOptions(List jvmOptions) {
if (hasContent(jvmOptions)) {
this.defaultJvmOptions = jvmOptions;
}
return this;
}
/**
* Sets the default arguments.
*
* @param args The arguments.
* @return The builder.
*/
public Builder defaultArgs(List args) {
if (hasContent(args)) {
this.defaultArgs = args;
}
return this;
}
/**
* Sets the default debug arguments used when starting the application with the {@code --debug} flag.
*
* @param debugOptions The options.
* @return The builder.
*/
public Builder defaultDebugOptions(List debugOptions) {
if (hasContent(debugOptions)) {
this.defaultDebugOptions = debugOptions;
}
return this;
}
/**
* Sets whether or not a CDS archive was installed.
*
* @param cdsInstalled {@code true} if installed.
* @return The builder.
*/
public Builder cdsInstalled(boolean cdsInstalled) {
this.cdsInstalled = cdsInstalled;
return this;
}
/**
* Sets whether or not a debug classes and module were installed.
*
* @param debugInstalled {@code true} if debug is installed.
* @return The builder.
*/
public Builder debugInstalled(boolean debugInstalled) {
this.debugInstalled = debugInstalled;
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;
}
/**
* Sets the template.
*
* @param template The template.
* @return The builder.
*/
public Builder template(Template template) {
this.template = template;
return this;
}
/**
* Sets the maximum number of seconds to wait for the application to start.
*
* @param maxAppStartSeconds The number of seconds.
* @return The builder.
*/
public Builder maxAppStartSeconds(int maxAppStartSeconds) {
this.maxAppStartSeconds = maxAppStartSeconds;
return this;
}
/**
* Builds and returns the instance.
*
* @return The instance.
* @throws IllegalArgumentException If a script cannot be created for the current platform.
*/
public StartScript build() {
if (installHomeDirectory == null) {
throw new IllegalStateException("installHomeDirectory is required");
}
if (mainJar == null) {
throw new IllegalStateException("mainJar is required");
}
this.scriptFile = scriptInstallDirectory.resolve(SCRIPT_FILE_NAME);
this.config = toConfig();
this.script = template().render(config);
return new StartScript(this);
}
private Template template() {
if (template == null) {
if (OS == Unknown) {
throw new PlatformNotSupportedError(config.toCommand());
} else {
return new StartScriptTemplate();
}
} else {
return template;
}
}
private TemplateConfig toConfig() {
return new TemplateConfig() {
@Override
public Path installHomeDirectory() {
return installHomeDirectory;
}
@Override
public Path scriptInstallDirectory() {
return scriptInstallDirectory;
}
@Override
public Path mainJar() {
return mainJar;
}
@Override
public List defaultJvmOptions() {
return defaultJvmOptions;
}
@Override
public List defaultDebugOptions() {
return defaultDebugOptions;
}
@Override
public List defaultArgs() {
return defaultArgs;
}
@Override
public boolean cdsInstalled() {
return cdsInstalled;
}
@Override
public boolean debugInstalled() {
return debugInstalled;
}
@Override
public String exitOnStartedValue() {
return exitOnStartedValue;
}
};
}
private static boolean hasContent(Collection> value) {
return value != null && !value.isEmpty();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy