
eu.cloudnetservice.node.service.defaults.JVMService Maven / Gradle / Ivy
/*
* Copyright 2019-2023 CloudNetService team & contributors
*
* 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 eu.cloudnetservice.node.service.defaults;
import com.google.common.primitives.Ints;
import eu.cloudnetservice.common.StringUtil;
import eu.cloudnetservice.common.collection.Pair;
import eu.cloudnetservice.common.function.ThrowableFunction;
import eu.cloudnetservice.common.io.FileUtil;
import eu.cloudnetservice.common.language.I18n;
import eu.cloudnetservice.common.log.LogManager;
import eu.cloudnetservice.common.log.Logger;
import eu.cloudnetservice.driver.channel.ChannelMessage;
import eu.cloudnetservice.driver.channel.ChannelMessageSender;
import eu.cloudnetservice.driver.event.EventManager;
import eu.cloudnetservice.driver.event.events.service.CloudServiceLogEntryEvent;
import eu.cloudnetservice.driver.network.buffer.DataBuf;
import eu.cloudnetservice.driver.network.def.NetworkConstants;
import eu.cloudnetservice.driver.service.ServiceConfiguration;
import eu.cloudnetservice.driver.service.ServiceEnvironment;
import eu.cloudnetservice.driver.service.ServiceEnvironmentType;
import eu.cloudnetservice.node.TickLoop;
import eu.cloudnetservice.node.config.Configuration;
import eu.cloudnetservice.node.event.service.CloudServicePostProcessStartEvent;
import eu.cloudnetservice.node.event.service.CloudServicePreProcessStartEvent;
import eu.cloudnetservice.node.service.CloudServiceManager;
import eu.cloudnetservice.node.service.ServiceConfigurationPreparer;
import eu.cloudnetservice.node.service.defaults.log.ProcessServiceLogCache;
import eu.cloudnetservice.node.version.ServiceVersionProvider;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
public class JVMService extends AbstractService {
protected static final Logger LOGGER = LogManager.logger(JVMService.class);
protected static final Pattern FILE_NUMBER_PATTERN = Pattern.compile("(\\d+).*");
protected static final Collection DEFAULT_JVM_SYSTEM_PROPERTIES = Arrays.asList(
"-Dfile.encoding=UTF-8",
"-Dlog4j2.formatMsgNoLookups=true",
"-DIReallyKnowWhatIAmDoingISwear=true",
"-Djline.terminal=jline.UnsupportedTerminal");
protected static final Path LIB_PATH = Path.of("launcher", "libs");
protected static final Path WRAPPER_TEMP_FILE = FileUtil.TEMP_DIR.resolve("caches").resolve("wrapper.jar");
protected volatile Process process;
public JVMService(
@NonNull TickLoop tickLoop,
@NonNull Configuration nodeConfig,
@NonNull ServiceConfiguration configuration,
@NonNull CloudServiceManager manager,
@NonNull EventManager eventManager,
@NonNull ServiceVersionProvider versionProvider,
@NonNull ServiceConfigurationPreparer serviceConfigurationPreparer
) {
super(tickLoop, nodeConfig, configuration, manager, eventManager, versionProvider, serviceConfigurationPreparer);
super.logCache = new ProcessServiceLogCache(() -> this.process, nodeConfig, this);
this.initLogHandler();
}
@Override
protected void startProcess() {
this.eventManager.callEvent(new CloudServicePreProcessStartEvent(this));
var environmentType = this.serviceConfiguration().serviceId().environment();
// load the wrapper information if possible
var wrapperInformation = this.prepareWrapperFile();
if (wrapperInformation == null) {
LOGGER.severe("Unable to load wrapper information for service startup");
return;
}
// load the application file information if possible
var applicationInformation = this.prepareApplicationFile(environmentType);
if (applicationInformation == null) {
LOGGER.severe(I18n.trans("cloudnet-service-jar-file-not-found-error", this.serviceReplacement()));
return;
}
// get the agent class of the application (if any)
var agentClass = applicationInformation.second().mainAttributes().getValue("Premain-Class");
if (agentClass == null) {
// some old versions named the agent class 'Launcher-Agent-Class' - try that
agentClass = applicationInformation.second().mainAttributes().getValue("Launcher-Agent-Class");
}
// prepare the full wrapper class path
var classPath = String.format(
"%s%s",
this.computeWrapperClassPath(wrapperInformation.first()),
wrapperInformation.first().toAbsolutePath());
// prepare the service startup
List arguments = new LinkedList<>();
// add the java command to start the service
var overriddenJavaCommand = this.serviceConfiguration().javaCommand();
arguments.add(overriddenJavaCommand == null ? this.configuration.javaCommand() : overriddenJavaCommand);
// add the jvm flags of the service configuration
arguments.addAll(this.cloudServiceManager().defaultJvmOptions());
arguments.addAll(this.serviceConfiguration().processConfig().jvmOptions());
// set the maximum heap memory setting. Xms matching Xmx because if not there is unused memory
arguments.add("-Xmx" + this.serviceConfiguration().processConfig().maxHeapMemorySize() + "M");
arguments.add("-Xms" + this.serviceConfiguration().processConfig().maxHeapMemorySize() + "M");
// override some default configuration options
arguments.addAll(DEFAULT_JVM_SYSTEM_PROPERTIES);
arguments.add("-javaagent:" + wrapperInformation.first().toAbsolutePath());
arguments.add("-Dcloudnet.wrapper.messages.language=" + I18n.language());
// fabric specific class path
arguments.add(String.format("-Dfabric.systemLibraries=%s", classPath));
// set the used host and port as system property
arguments.add("-Dservice.bind.host=" + this.serviceConfiguration().hostAddress());
arguments.add("-Dservice.bind.port=" + this.serviceConfiguration().port());
// add the class path and the main class of the wrapper
arguments.add("-cp");
arguments.add(classPath);
arguments.add(wrapperInformation.second().getValue("Main-Class")); // the main class we want to invoke first
// add all internal process parameters (they will be removed by the wrapper before starting the application)
arguments.add(applicationInformation.second().mainAttributes().getValue("Main-Class"));
arguments.add(String.valueOf(agentClass)); // the agent class might be null
arguments.add(applicationInformation.first().toAbsolutePath().toString());
arguments.add(Boolean.toString(applicationInformation.second().preloadJarContent()));
// add all process parameters
arguments.addAll(environmentType.defaultProcessArguments());
arguments.addAll(this.serviceConfiguration().processConfig().processParameters());
// try to start the process like that
this.doStartProcess(arguments, wrapperInformation.first(), applicationInformation.first());
}
@Override
protected void stopProcess() {
if (this.process != null) {
// try to send a shutdown command
this.runCommand("end");
this.runCommand("stop");
try {
// wait until the process termination seconds exceeded
if (this.process.waitFor(this.configuration.processTerminationTimeoutSeconds(), TimeUnit.SECONDS)) {
this.process.exitValue(); // validation that the process terminated
this.process = null; // reset as there is no fall-through
return;
}
} catch (IllegalThreadStateException | InterruptedException ignored) { // force shutdown the process
}
// force destroy the process now - not much we can do here more than that
this.process.toHandle().destroyForcibly();
this.process = null;
}
}
@Override
public void runCommand(@NonNull String command) {
if (this.process != null) {
try {
var out = this.process.getOutputStream();
// write & flush
out.write((command + "\n").getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (IOException exception) {
LOGGER.finer("Unable to dispatch command %s on service %s", exception, command, this.serviceId());
}
}
}
@Override
public @NonNull String runtime() {
return "jvm";
}
@Override
public boolean alive() {
return this.process != null && this.process.toHandle().isAlive();
}
protected void doStartProcess(
@NonNull List arguments,
@NonNull Path wrapperPath,
@NonNull Path applicationFilePath
) {
try {
// prepare the builder and apply the environment variables to it
var builder = new ProcessBuilder(arguments).directory(this.serviceDirectory.toFile());
for (var entry : this.serviceConfiguration().environmentVariables().entrySet()) {
// there is no consensus forcing the key of an environment variable to be uppercase
// however, docker rejects environment variables with a non-uppercase key, so to keep
// consistency between service types we force the uppercase keys here as well
builder.environment().put(StringUtil.toUpper(entry.getKey()), entry.getValue());
}
// start the process and fire the post start event
this.process = builder.start();
this.eventManager.callEvent(new CloudServicePostProcessStartEvent(this));
} catch (IOException exception) {
LOGGER.severe("Unable to start process in %s with command line %s",
exception,
this.serviceDirectory,
String.join(" ", arguments));
}
}
protected void initLogHandler() {
super.logCache.addHandler(($, line, stderr) -> {
for (var logTarget : super.logTargets) {
if (logTarget.first().equals(ChannelMessageSender.self().toTarget())) {
// the current target is the node this service is running on, print it directly here
this.eventManager.callEvent(logTarget.second(), new CloudServiceLogEntryEvent(
this.currentServiceInfo,
line,
stderr ? CloudServiceLogEntryEvent.StreamType.STDERR : CloudServiceLogEntryEvent.StreamType.STDOUT));
} else {
// the listener is listening remotely, send the line to the network component
ChannelMessage.builder()
.target(logTarget.first())
.channel(NetworkConstants.INTERNAL_MSG_CHANNEL)
.message("screen_new_line")
.buffer(DataBuf.empty()
.writeObject(this.currentServiceInfo)
.writeString(logTarget.second())
.writeString(line)
.writeBoolean(stderr))
.build()
.send();
}
}
});
}
protected @Nullable Pair prepareWrapperFile() {
// check if the wrapper file is there - unpack it if not
if (Files.notExists(WRAPPER_TEMP_FILE)) {
FileUtil.createDirectory(WRAPPER_TEMP_FILE.getParent());
try (var stream = JVMService.class.getClassLoader().getResourceAsStream("wrapper.jar")) {
// ensure that the wrapper file is there
if (stream == null) {
throw new IllegalStateException("Build-in \"wrapper.jar\" missing, unable to start jvm based services");
}
// copy the wrapper file to the output directory
Files.copy(stream, WRAPPER_TEMP_FILE, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
LOGGER.severe("Unable to copy \"wrapper.jar\" to %s", exception, WRAPPER_TEMP_FILE);
}
}
// read the main class
return this.completeJarAttributeInformation(
WRAPPER_TEMP_FILE,
file -> file.getManifest().getMainAttributes());
}
protected @Nullable Pair prepareApplicationFile(
@NonNull ServiceEnvironmentType environmentType
) {
// collect all names of environment names
var environments = this.serviceVersionProvider.serviceVersionTypes().values().stream()
.filter(environment -> environment.environmentType().equals(environmentType.name()))
.map(ServiceEnvironment::name)
.collect(Collectors.collectingAndThen(Collectors.toSet(), result -> {
// add a default fallback value which applied to all environments
result.add("application");
return result;
}));
try {
// walk the file tree and filter the best application file
return Files.walk(this.serviceDirectory, 1)
.filter(path -> {
var filename = path.getFileName().toString();
// check if the file is a jar file - it must end with '.jar' for that
if (!filename.endsWith(".jar")) {
return false;
}
// search if any environment is in the name of the file
for (var environment : environments) {
if (filename.contains(environment)) {
return true;
}
}
// not an application file for the environment
return false;
}).min((left, right) -> {
// get the first number from the left path
var leftMatcher = FILE_NUMBER_PATTERN.matcher(left.getFileName().toString());
// no match -> neutral
if (!leftMatcher.matches()) {
return 0;
}
// get the first number from the right patch
var rightMatcher = FILE_NUMBER_PATTERN.matcher(right.getFileName().toString());
// no match -> neutral
if (!rightMatcher.matches()) {
return 0;
}
// extract the numbers
var leftNumber = Ints.tryParse(leftMatcher.group(1));
var rightNumber = Ints.tryParse(rightMatcher.group(1));
// compare both of the numbers
return leftNumber == null || rightNumber == null ? 0 : Integer.compare(leftNumber, rightNumber);
})
.map(path -> this.completeJarAttributeInformation(
path,
file -> new ApplicationStartupInformation(
file.getEntry("META-INF/versions.list") != null,
file.getManifest().getMainAttributes())
)).orElse(null);
} catch (IOException exception) {
LOGGER.severe("Unable to find application file information in %s for environment %s",
exception,
this.serviceDirectory,
environmentType);
return null;
}
}
protected @Nullable Pair completeJarAttributeInformation(
@NonNull Path jarFilePath,
@NonNull ThrowableFunction mapper
) {
// open the file and lookup the main class
try (var jarFile = new JarFile(jarFilePath.toFile())) {
return new Pair<>(jarFilePath, mapper.apply(jarFile));
} catch (IOException exception) {
LOGGER.severe("Unable to open wrapper file at %s for reading: ", exception, jarFilePath);
return null;
}
}
protected @NonNull String computeWrapperClassPath(@NonNull Path wrapperPath) {
var builder = new StringBuilder();
FileUtil.openZipFile(wrapperPath, fs -> {
// get the wrapper cnl file and check if it is available
var wrapperCnl = fs.getPath("wrapper.cnl");
if (Files.exists(wrapperCnl)) {
Files.lines(wrapperCnl)
.filter(line -> line.startsWith("include "))
.map(line -> line.split(" "))
.filter(parts -> parts.length >= 6)
.map(parts -> {
// ///--.jar
var path = String.format(
"%s/%s/%s/%s-%s%s.jar",
parts[2].replace('.', '/'),
parts[3],
parts[4],
parts[3],
parts[5],
parts.length == 8 ? "-" + parts[7] : "");
return LIB_PATH.resolve(path);
}).forEach(path -> builder.append(path.toAbsolutePath()).append(File.pathSeparatorChar));
}
});
// contains all paths we need now
return builder.toString();
}
protected record ApplicationStartupInformation(boolean preloadJarContent, @NonNull Attributes mainAttributes) {
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy