Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ fun TaskProvider<out Jar>.applyJarMetadata(
"Implementation-Vendor" to "CloudNetService",
"Implementation-Title" to Versions.CLOUDNET_CODE_NAME,
)
preMain?.let { manifest.attributes("Premain-Class" to it) }
preMain?.let {
manifest.attributes(
"Premain-Class" to it,
"Can-Redefine-Classes" to true,
"Can-Retransform-Classes" to true,
"Can-Set-Native-Method-Prefix" to true
)
}

val commit = git.commit()
val branchName = git.branchName()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class DefaultRegisteredEventListener implements RegisteredEventListener {
*/
@Override
public void fireEvent(@NonNull Event event) {
LOGGER.debug(
LOGGER.trace(
"Calling event {} on listener {}",
event.getClass().getName(),
this.instance().getClass().getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2019-present 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.event.service;

import eu.cloudnetservice.node.service.CloudService;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import lombok.NonNull;
import org.jetbrains.annotations.UnmodifiableView;

/**
* An event which is called after the service class path got constructed, but before it is being used to start the
* service. Removing elements from the class path is not possible.
*
* @since 4.0
*/
public final class CloudServiceJvmClassPathConstructEvent extends CloudServiceEvent {

private final Collection<Path> classPath;

/**
* Constructs a new cloud service jvm class path event.
*
* @param service the service for which the class path got constructed.
* @param classPath the constructed class path.
*/
public CloudServiceJvmClassPathConstructEvent(@NonNull CloudService service, @NonNull Collection<Path> classPath) {
super(service);
this.classPath = classPath;
}

/**
* Gets the constructed class path for the service. This set is an unmodifiable view of the class path.
*
* @return the constructed class path.
*/
@UnmodifiableView
public @NonNull Collection<Path> classPath() {
return Collections.unmodifiableCollection(this.classPath);
}

/**
* Adds a new entry to the class path of the service.
*
* @param path the path to add.
* @throws NullPointerException if the given path is null.
*/
public void addClassPathEntry(@NonNull Path path) {
this.classPath.add(path);
}

/**
* Adds multiple new entries to the class path of the service.
*
* @param paths the paths to add.
* @throws NullPointerException if the given collection is null.
*/
public void addClassPathEntries(@NonNull Collection<Path> paths) {
this.classPath.addAll(paths);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,40 @@

package eu.cloudnetservice.node.impl.service.defaults;

import com.google.common.base.Preconditions;
import com.google.common.primitives.Ints;
import eu.cloudnetservice.driver.event.EventManager;
import eu.cloudnetservice.driver.language.I18n;
import eu.cloudnetservice.driver.service.ServiceConfiguration;
import eu.cloudnetservice.driver.service.ServiceEnvironment;
import eu.cloudnetservice.driver.service.ServiceEnvironmentType;
import eu.cloudnetservice.node.config.Configuration;
import eu.cloudnetservice.node.event.service.CloudServiceJvmClassPathConstructEvent;
import eu.cloudnetservice.node.event.service.CloudServicePostProcessStartEvent;
import eu.cloudnetservice.node.event.service.CloudServicePreProcessStartEvent;
import eu.cloudnetservice.node.impl.service.InternalCloudServiceManager;
import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogCache;
import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogReadScheduler;
import eu.cloudnetservice.node.impl.service.defaults.wrapper.WrapperFileProvider;
import eu.cloudnetservice.node.impl.tick.DefaultTickLoop;
import eu.cloudnetservice.node.impl.version.ServiceVersionProvider;
import eu.cloudnetservice.node.service.ServiceConfigurationPreparer;
import eu.cloudnetservice.node.service.ServiceConsoleLogCache;
import eu.cloudnetservice.utils.base.StringUtil;
import eu.cloudnetservice.utils.base.io.FileUtil;
import io.vavr.CheckedFunction1;
import io.vavr.Tuple2;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -61,7 +59,6 @@
public class JVMService extends AbstractService {

protected static final Logger LOGGER = LoggerFactory.getLogger(JVMService.class);
protected static final Pattern FILE_NUMBER_PATTERN = Pattern.compile("(\\d+).*");
protected static final Collection<String> DEFAULT_JVM_SYSTEM_PROPERTIES = Arrays.asList(
"--enable-preview",
"-Dfile.encoding=UTF-8",
Expand All @@ -70,7 +67,6 @@ public class JVMService extends AbstractService {
"-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;

Expand Down Expand Up @@ -139,17 +135,20 @@ protected void startProcess() {
}

// get the agent class of the application (if any)
var agentClass = applicationInformation._2().mainAttributes().getValue("Premain-Class");
if (agentClass == null) {
// some old versions named the agent class 'Launcher-Agent-Class' - try that
agentClass = applicationInformation._2().mainAttributes().getValue("Launcher-Agent-Class");
}
var agentClass = applicationInformation.mainAttributes().getValue("Launcher-Agent-Class");

// prepare the full wrapper class path
var classPath = String.format(
"%s%s",
this.computeWrapperClassPath(wrapperInformation._1()),
wrapperInformation._1().toAbsolutePath());
List<Path> classPathBuilder = new ArrayList<>();
this.computeWrapperClassPath(classPathBuilder, wrapperInformation.path());
classPathBuilder.add(wrapperInformation.path());
classPathBuilder.add(applicationInformation.path());
this.eventManager.callEvent(new CloudServiceJvmClassPathConstructEvent(this, classPathBuilder));
var classPath = classPathBuilder.stream()
.map(Path::toAbsolutePath)
.map(Path::normalize)
.map(Path::toString)
.distinct()
.collect(Collectors.joining(File.pathSeparator));

// prepare the service startup
List<String> arguments = new LinkedList<>();
Expand All @@ -168,33 +167,27 @@ protected void startProcess() {

// override some default configuration options
arguments.addAll(DEFAULT_JVM_SYSTEM_PROPERTIES);
arguments.add("-javaagent:" + wrapperInformation._1().toAbsolutePath());
arguments.add("-javaagent:" + wrapperInformation.path().toAbsolutePath());
arguments.add("-Dcloudnet.wrapper.messages.language=" + super.i18n.selectedLanguage().toLanguageTag());

// fabric specific class path
arguments.add(String.format("-Dfabric.systemLibraries=%s", classPath));
if (agentClass != null) {
arguments.add("-Dcloudnet.wrapper.launcher-agent-class=" + agentClass);
}

// 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
// add the class path and the main class of the application
arguments.add("-cp");
arguments.add(classPath);
arguments.add(wrapperInformation._2().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._2().mainAttributes().getValue("Main-Class"));
arguments.add(String.valueOf(agentClass)); // the agent class might be null
arguments.add(applicationInformation._1().toAbsolutePath().toString());
arguments.add(Boolean.toString(applicationInformation._2().preloadJarContent()));
arguments.add(applicationInformation.mainAttributes().getValue("Main-Class"));

// 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._1(), applicationInformation._1());
this.doStartProcess(arguments, wrapperInformation.path(), applicationInformation.path());
}

@Override
Expand Down Expand Up @@ -287,28 +280,17 @@ protected void doStartProcess(
}
}

protected @Nullable Tuple2<Path, Attributes> 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.error("Unable to copy \"wrapper.jar\" to {}", WRAPPER_TEMP_FILE, exception);
}
}
// read the main class
return this.completeJarAttributeInformation(
WRAPPER_TEMP_FILE,

protected @Nullable JarFileData prepareWrapperFile() {
var wrapperTempPath = WrapperFileProvider.unpackWrapperFile();
var mainAttributes = this.completeJarAttributeInformation(
wrapperTempPath,
file -> file.getManifest().getMainAttributes());
Objects.requireNonNull(mainAttributes, "Wrapper jar does not contain a manifest");
return new JarFileData(wrapperTempPath, mainAttributes);
}

protected @Nullable Tuple2<Path, ApplicationStartupInformation> prepareApplicationFile(
protected @Nullable JarFileData prepareApplicationFile(
@NonNull ServiceEnvironmentType environmentType
) {
// collect all names of environment names
Expand All @@ -326,45 +308,29 @@ protected void doStartProcess(
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;
}
})
.map(path -> {
var manifest = this.completeJarAttributeInformation(path, JarFile::getManifest);
Objects.requireNonNull(manifest, "Application jar does not contain a manifest");

// 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;
}
var mainClass = manifest.getMainAttributes().getValue("Main-Class");
Objects.requireNonNull(mainClass, "Application jar manifest does not contain a Main-Class");

// 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);
return new JarFileData(path, manifest.getMainAttributes());
})
.map(path -> this.completeJarAttributeInformation(
path,
file -> new ApplicationStartupInformation(
file.getEntry("META-INF/versions.list") != null,
this.validateManifest(file.getManifest()).getMainAttributes())
)).orElse(null);
.findFirst()
.orElse(null);
} catch (IOException exception) {
LOGGER.error(
"Unable to find application file information in {} for environment {}",
Expand All @@ -375,23 +341,21 @@ protected void doStartProcess(
}
}

protected @Nullable <T> Tuple2<Path, T> completeJarAttributeInformation(
protected @Nullable <T> T completeJarAttributeInformation(
@NonNull Path jarFilePath,
@NonNull CheckedFunction1<JarFile, T> mapper
) {
// open the file and lookup the main class
try (var jarFile = new JarFile(jarFilePath.toFile())) {
return new Tuple2<>(jarFilePath, mapper.apply(jarFile));
return mapper.apply(jarFile);
} catch (Throwable exception) {
LOGGER.error("Unable to open wrapper file at {} for reading: ", jarFilePath, exception);
return null;
}
}

protected @NonNull String computeWrapperClassPath(@NonNull Path wrapperPath) {
var builder = new StringBuilder();
protected void computeWrapperClassPath(@NonNull Collection<Path> classPath, @NonNull Path wrapperPath) {
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)
Expand All @@ -409,24 +373,12 @@ protected void doStartProcess(
parts[5],
parts.length == 8 ? "-" + parts[7] : "");
return LIB_PATH.resolve(path);
}).forEach(path -> builder.append(path.toAbsolutePath()).append(File.pathSeparatorChar));
}).forEach(classPath::add);
}
});
// contains all paths we need now
return builder.toString();
}

protected @NonNull Manifest validateManifest(@Nullable Manifest manifest) {
// make sure that we have a manifest at all
Preconditions.checkNotNull(manifest, "Application jar does not contain a manifest.");
// make sure that the manifest at least contains a main class
Preconditions.checkNotNull(
manifest.getMainAttributes().getValue("Main-Class"),
"Application jar manifest does not contain a Main-Class.");
return manifest;
}

protected record ApplicationStartupInformation(boolean preloadJarContent, @NonNull Attributes mainAttributes) {
protected record JarFileData(@NonNull Path path, @NonNull Attributes mainAttributes) {

}
}
Loading
Loading