Skip to content

Commit 207a828

Browse files
0utplayderklaro
andauthored
feat: make the cloudnet wrapper a java agent (#1839)
### Motivation Having the wrapper as java agent instead of a standalone application that boots the regular application resolves issues where e.g. the application depends on SPI to get some implementation, which was not possible with the current wrapper setup. ### Modification Re-designed the wrapper to be a java agent that is attached instead of a standalone application ### Result The wrapper is an agent and we can start taking a look into sponge support again --------- Co-authored-by: Pasqual Koschmieder <git@derklaro.dev>
1 parent 0824fe1 commit 207a828

File tree

14 files changed

+401
-583
lines changed

14 files changed

+401
-583
lines changed

build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Extensions.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ fun TaskProvider<out Jar>.applyJarMetadata(
4343
"Implementation-Vendor" to "CloudNetService",
4444
"Implementation-Title" to Versions.CLOUDNET_CODE_NAME,
4545
)
46-
preMain?.let { manifest.attributes("Premain-Class" to it) }
46+
preMain?.let {
47+
manifest.attributes(
48+
"Premain-Class" to it,
49+
"Can-Redefine-Classes" to true,
50+
"Can-Retransform-Classes" to true,
51+
"Can-Set-Native-Method-Prefix" to true,
52+
)
53+
}
4754

4855
val commit = git.commit()
4956
val branchName = git.branchName()

driver/impl/src/main/java/eu/cloudnetservice/driver/impl/event/DefaultRegisteredEventListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ final class DefaultRegisteredEventListener implements RegisteredEventListener {
8787
*/
8888
@Override
8989
public void fireEvent(@NonNull Event event) {
90-
LOGGER.debug(
90+
LOGGER.trace(
9191
"Calling event {} on listener {}",
9292
event.getClass().getName(),
9393
this.instance().getClass().getName());
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2019-present CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.node.event.service;
18+
19+
import eu.cloudnetservice.node.service.CloudService;
20+
import java.nio.file.Path;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import lombok.NonNull;
24+
import org.jetbrains.annotations.UnmodifiableView;
25+
26+
/**
27+
* An event which is called after the service class path got constructed, but before it is being used to start the
28+
* service. Removing elements from the class path is not possible.
29+
*
30+
* @since 4.0
31+
*/
32+
public final class CloudServiceJvmClassPathConstructEvent extends CloudServiceEvent {
33+
34+
private final Collection<Path> classPath;
35+
36+
/**
37+
* Constructs a new cloud service jvm class path event.
38+
*
39+
* @param service the service for which the class path got constructed.
40+
* @param classPath the constructed class path.
41+
*/
42+
public CloudServiceJvmClassPathConstructEvent(@NonNull CloudService service, @NonNull Collection<Path> classPath) {
43+
super(service);
44+
this.classPath = classPath;
45+
}
46+
47+
/**
48+
* Gets the constructed class path for the service. This set is an unmodifiable view of the class path.
49+
*
50+
* @return the constructed class path.
51+
*/
52+
@UnmodifiableView
53+
public @NonNull Collection<Path> classPath() {
54+
return Collections.unmodifiableCollection(this.classPath);
55+
}
56+
57+
/**
58+
* Adds a new entry to the class path of the service.
59+
*
60+
* @param path the path to add.
61+
* @throws NullPointerException if the given path is null.
62+
*/
63+
public void addClassPathEntry(@NonNull Path path) {
64+
this.classPath.add(path);
65+
}
66+
67+
/**
68+
* Adds multiple new entries to the class path of the service.
69+
*
70+
* @param paths the paths to add.
71+
* @throws NullPointerException if the given collection is null.
72+
*/
73+
public void addClassPathEntries(@NonNull Collection<Path> paths) {
74+
this.classPath.addAll(paths);
75+
}
76+
}

node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/JVMService.java

Lines changed: 47 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,40 @@
1616

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

19-
import com.google.common.base.Preconditions;
20-
import com.google.common.primitives.Ints;
2119
import eu.cloudnetservice.driver.event.EventManager;
2220
import eu.cloudnetservice.driver.language.I18n;
2321
import eu.cloudnetservice.driver.service.ServiceConfiguration;
2422
import eu.cloudnetservice.driver.service.ServiceEnvironment;
2523
import eu.cloudnetservice.driver.service.ServiceEnvironmentType;
2624
import eu.cloudnetservice.node.config.Configuration;
25+
import eu.cloudnetservice.node.event.service.CloudServiceJvmClassPathConstructEvent;
2726
import eu.cloudnetservice.node.event.service.CloudServicePostProcessStartEvent;
2827
import eu.cloudnetservice.node.event.service.CloudServicePreProcessStartEvent;
2928
import eu.cloudnetservice.node.impl.service.InternalCloudServiceManager;
3029
import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogCache;
3130
import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogReadScheduler;
31+
import eu.cloudnetservice.node.impl.service.defaults.wrapper.WrapperFileProvider;
3232
import eu.cloudnetservice.node.impl.tick.DefaultTickLoop;
3333
import eu.cloudnetservice.node.impl.version.ServiceVersionProvider;
3434
import eu.cloudnetservice.node.service.ServiceConfigurationPreparer;
3535
import eu.cloudnetservice.node.service.ServiceConsoleLogCache;
3636
import eu.cloudnetservice.utils.base.StringUtil;
3737
import eu.cloudnetservice.utils.base.io.FileUtil;
3838
import io.vavr.CheckedFunction1;
39-
import io.vavr.Tuple2;
4039
import java.io.File;
4140
import java.io.IOException;
4241
import java.nio.charset.StandardCharsets;
4342
import java.nio.file.Files;
4443
import java.nio.file.Path;
45-
import java.nio.file.StandardCopyOption;
44+
import java.util.ArrayList;
4645
import java.util.Arrays;
4746
import java.util.Collection;
4847
import java.util.LinkedList;
4948
import java.util.List;
49+
import java.util.Objects;
5050
import java.util.concurrent.TimeUnit;
5151
import java.util.jar.Attributes;
5252
import java.util.jar.JarFile;
53-
import java.util.jar.Manifest;
54-
import java.util.regex.Pattern;
5553
import java.util.stream.Collectors;
5654
import lombok.NonNull;
5755
import org.jetbrains.annotations.Nullable;
@@ -61,7 +59,6 @@
6159
public class JVMService extends AbstractService {
6260

6361
protected static final Logger LOGGER = LoggerFactory.getLogger(JVMService.class);
64-
protected static final Pattern FILE_NUMBER_PATTERN = Pattern.compile("(\\d+).*");
6562
protected static final Collection<String> DEFAULT_JVM_SYSTEM_PROPERTIES = Arrays.asList(
6663
"--enable-preview",
6764
"-Dfile.encoding=UTF-8",
@@ -70,7 +67,6 @@ public class JVMService extends AbstractService {
7067
"-Djline.terminal=jline.UnsupportedTerminal");
7168

7269
protected static final Path LIB_PATH = Path.of("launcher", "libs");
73-
protected static final Path WRAPPER_TEMP_FILE = FileUtil.TEMP_DIR.resolve("caches").resolve("wrapper.jar");
7470

7571
protected volatile Process process;
7672

@@ -139,17 +135,20 @@ protected void startProcess() {
139135
}
140136

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

148140
// prepare the full wrapper class path
149-
var classPath = String.format(
150-
"%s%s",
151-
this.computeWrapperClassPath(wrapperInformation._1()),
152-
wrapperInformation._1().toAbsolutePath());
141+
List<Path> classPathBuilder = new ArrayList<>();
142+
this.computeWrapperClassPath(classPathBuilder, wrapperInformation.path());
143+
classPathBuilder.add(wrapperInformation.path());
144+
classPathBuilder.add(applicationInformation.path());
145+
this.eventManager.callEvent(new CloudServiceJvmClassPathConstructEvent(this, classPathBuilder));
146+
var classPath = classPathBuilder.stream()
147+
.map(Path::toAbsolutePath)
148+
.map(Path::normalize)
149+
.map(Path::toString)
150+
.distinct()
151+
.collect(Collectors.joining(File.pathSeparator));
153152

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

169168
// override some default configuration options
170169
arguments.addAll(DEFAULT_JVM_SYSTEM_PROPERTIES);
171-
arguments.add("-javaagent:" + wrapperInformation._1().toAbsolutePath());
170+
arguments.add("-javaagent:" + wrapperInformation.path().toAbsolutePath());
172171
arguments.add("-Dcloudnet.wrapper.messages.language=" + super.i18n.selectedLanguage().toLanguageTag());
173-
174-
// fabric specific class path
175-
arguments.add(String.format("-Dfabric.systemLibraries=%s", classPath));
172+
if (agentClass != null) {
173+
arguments.add("-Dcloudnet.wrapper.launcher-agent-class=" + agentClass);
174+
}
176175

177176
// set the used host and port as system property
178177
arguments.add("-Dservice.bind.host=" + this.serviceConfiguration().hostAddress());
179178
arguments.add("-Dservice.bind.port=" + this.serviceConfiguration().port());
180179

181-
// add the class path and the main class of the wrapper
180+
// add the class path and the main class of the application
182181
arguments.add("-cp");
183182
arguments.add(classPath);
184-
arguments.add(wrapperInformation._2().getValue("Main-Class")); // the main class we want to invoke first
185-
186-
// add all internal process parameters (they will be removed by the wrapper before starting the application)
187-
arguments.add(applicationInformation._2().mainAttributes().getValue("Main-Class"));
188-
arguments.add(String.valueOf(agentClass)); // the agent class might be null
189-
arguments.add(applicationInformation._1().toAbsolutePath().toString());
190-
arguments.add(Boolean.toString(applicationInformation._2().preloadJarContent()));
183+
arguments.add(applicationInformation.mainAttributes().getValue(Attributes.Name.MAIN_CLASS));
191184

192185
// add all process parameters
193186
arguments.addAll(environmentType.defaultProcessArguments());
194187
arguments.addAll(this.serviceConfiguration().processConfig().processParameters());
195188

196189
// try to start the process like that
197-
this.doStartProcess(arguments, wrapperInformation._1(), applicationInformation._1());
190+
this.doStartProcess(arguments, wrapperInformation.path(), applicationInformation.path());
198191
}
199192

200193
@Override
@@ -287,28 +280,17 @@ protected void doStartProcess(
287280
}
288281
}
289282

290-
protected @Nullable Tuple2<Path, Attributes> prepareWrapperFile() {
291-
// check if the wrapper file is there - unpack it if not
292-
if (Files.notExists(WRAPPER_TEMP_FILE)) {
293-
FileUtil.createDirectory(WRAPPER_TEMP_FILE.getParent());
294-
try (var stream = JVMService.class.getClassLoader().getResourceAsStream("wrapper.jar")) {
295-
// ensure that the wrapper file is there
296-
if (stream == null) {
297-
throw new IllegalStateException("Build-in \"wrapper.jar\" missing, unable to start jvm based services");
298-
}
299-
// copy the wrapper file to the output directory
300-
Files.copy(stream, WRAPPER_TEMP_FILE, StandardCopyOption.REPLACE_EXISTING);
301-
} catch (IOException exception) {
302-
LOGGER.error("Unable to copy \"wrapper.jar\" to {}", WRAPPER_TEMP_FILE, exception);
303-
}
304-
}
305-
// read the main class
306-
return this.completeJarAttributeInformation(
307-
WRAPPER_TEMP_FILE,
283+
284+
protected @Nullable JarFileData prepareWrapperFile() {
285+
var wrapperTempPath = WrapperFileProvider.unpackWrapperFile();
286+
var mainAttributes = this.extractFromJarFile(
287+
wrapperTempPath,
308288
file -> file.getManifest().getMainAttributes());
289+
Objects.requireNonNull(mainAttributes, "Wrapper jar does not contain a manifest");
290+
return new JarFileData(wrapperTempPath, mainAttributes);
309291
}
310292

311-
protected @Nullable Tuple2<Path, ApplicationStartupInformation> prepareApplicationFile(
293+
protected @Nullable JarFileData prepareApplicationFile(
312294
@NonNull ServiceEnvironmentType environmentType
313295
) {
314296
// collect all names of environment names
@@ -326,45 +308,29 @@ protected void doStartProcess(
326308
return Files.walk(this.serviceDirectory, 1)
327309
.filter(path -> {
328310
var filename = path.getFileName().toString();
329-
// check if the file is a jar file - it must end with '.jar' for that
330311
if (!filename.endsWith(".jar")) {
331312
return false;
332313
}
333-
// search if any environment is in the name of the file
314+
334315
for (var environment : environments) {
335316
if (filename.contains(environment)) {
336317
return true;
337318
}
338319
}
339-
// not an application file for the environment
320+
340321
return false;
341-
}).min((left, right) -> {
342-
// get the first number from the left path
343-
var leftMatcher = FILE_NUMBER_PATTERN.matcher(left.getFileName().toString());
344-
// no match -> neutral
345-
if (!leftMatcher.matches()) {
346-
return 0;
347-
}
322+
})
323+
.map(path -> {
324+
var manifest = this.extractFromJarFile(path, JarFile::getManifest);
325+
Objects.requireNonNull(manifest, "Application jar does not contain a manifest");
348326

349-
// get the first number from the right patch
350-
var rightMatcher = FILE_NUMBER_PATTERN.matcher(right.getFileName().toString());
351-
// no match -> neutral
352-
if (!rightMatcher.matches()) {
353-
return 0;
354-
}
327+
var mainClass = manifest.getMainAttributes().getValue("Main-Class");
328+
Objects.requireNonNull(mainClass, "Application jar manifest does not contain a Main-Class");
355329

356-
// extract the numbers
357-
var leftNumber = Ints.tryParse(leftMatcher.group(1));
358-
var rightNumber = Ints.tryParse(rightMatcher.group(1));
359-
// compare both of the numbers
360-
return leftNumber == null || rightNumber == null ? 0 : Integer.compare(leftNumber, rightNumber);
330+
return new JarFileData(path, manifest.getMainAttributes());
361331
})
362-
.map(path -> this.completeJarAttributeInformation(
363-
path,
364-
file -> new ApplicationStartupInformation(
365-
file.getEntry("META-INF/versions.list") != null,
366-
this.validateManifest(file.getManifest()).getMainAttributes())
367-
)).orElse(null);
332+
.findFirst()
333+
.orElse(null);
368334
} catch (IOException exception) {
369335
LOGGER.error(
370336
"Unable to find application file information in {} for environment {}",
@@ -375,23 +341,21 @@ protected void doStartProcess(
375341
}
376342
}
377343

378-
protected @Nullable <T> Tuple2<Path, T> completeJarAttributeInformation(
344+
protected @Nullable <T> T extractFromJarFile(
379345
@NonNull Path jarFilePath,
380346
@NonNull CheckedFunction1<JarFile, T> mapper
381347
) {
382348
// open the file and lookup the main class
383349
try (var jarFile = new JarFile(jarFilePath.toFile())) {
384-
return new Tuple2<>(jarFilePath, mapper.apply(jarFile));
350+
return mapper.apply(jarFile);
385351
} catch (Throwable exception) {
386352
LOGGER.error("Unable to open wrapper file at {} for reading: ", jarFilePath, exception);
387353
return null;
388354
}
389355
}
390356

391-
protected @NonNull String computeWrapperClassPath(@NonNull Path wrapperPath) {
392-
var builder = new StringBuilder();
357+
protected void computeWrapperClassPath(@NonNull Collection<Path> classPath, @NonNull Path wrapperPath) {
393358
FileUtil.openZipFile(wrapperPath, fs -> {
394-
// get the wrapper cnl file and check if it is available
395359
var wrapperCnl = fs.getPath("wrapper.cnl");
396360
if (Files.exists(wrapperCnl)) {
397361
Files.lines(wrapperCnl)
@@ -409,24 +373,12 @@ protected void doStartProcess(
409373
parts[5],
410374
parts.length == 8 ? "-" + parts[7] : "");
411375
return LIB_PATH.resolve(path);
412-
}).forEach(path -> builder.append(path.toAbsolutePath()).append(File.pathSeparatorChar));
376+
}).forEach(classPath::add);
413377
}
414378
});
415-
// contains all paths we need now
416-
return builder.toString();
417-
}
418-
419-
protected @NonNull Manifest validateManifest(@Nullable Manifest manifest) {
420-
// make sure that we have a manifest at all
421-
Preconditions.checkNotNull(manifest, "Application jar does not contain a manifest.");
422-
// make sure that the manifest at least contains a main class
423-
Preconditions.checkNotNull(
424-
manifest.getMainAttributes().getValue("Main-Class"),
425-
"Application jar manifest does not contain a Main-Class.");
426-
return manifest;
427379
}
428380

429-
protected record ApplicationStartupInformation(boolean preloadJarContent, @NonNull Attributes mainAttributes) {
381+
protected record JarFileData(@NonNull Path path, @NonNull Attributes mainAttributes) {
430382

431383
}
432384
}

0 commit comments

Comments
 (0)