mirror of
https://github.com/GravitLauncher/Launcher
synced 2024-11-15 03:31:15 +03:00
[FEATURE] Unified launch
This commit is contained in:
parent
429c7a45c4
commit
c1df548258
21 changed files with 725 additions and 284 deletions
|
@ -1,6 +1,7 @@
|
||||||
package pro.gravit.launcher.debug;
|
package pro.gravit.launcher.debug;
|
||||||
|
|
||||||
import pro.gravit.launcher.ClientPermissions;
|
import pro.gravit.launcher.ClientPermissions;
|
||||||
|
import pro.gravit.launcher.Launcher;
|
||||||
import pro.gravit.launcher.LauncherEngine;
|
import pro.gravit.launcher.LauncherEngine;
|
||||||
import pro.gravit.launcher.api.AuthService;
|
import pro.gravit.launcher.api.AuthService;
|
||||||
import pro.gravit.launcher.events.request.AuthRequestEvent;
|
import pro.gravit.launcher.events.request.AuthRequestEvent;
|
||||||
|
@ -10,10 +11,17 @@
|
||||||
import pro.gravit.launcher.request.Request;
|
import pro.gravit.launcher.request.Request;
|
||||||
import pro.gravit.launcher.request.auth.AuthRequest;
|
import pro.gravit.launcher.request.auth.AuthRequest;
|
||||||
import pro.gravit.launcher.request.update.ProfilesRequest;
|
import pro.gravit.launcher.request.update.ProfilesRequest;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
import pro.gravit.utils.helper.LogHelper;
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
|
import pro.gravit.utils.launch.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ClientRuntimeProvider implements RuntimeProvider {
|
public class ClientRuntimeProvider implements RuntimeProvider {
|
||||||
|
@ -32,6 +40,11 @@ public void run(String[] args) {
|
||||||
long expire = Long.parseLong(System.getProperty("launcher.runtime.auth.expire", "0"));
|
long expire = Long.parseLong(System.getProperty("launcher.runtime.auth.expire", "0"));
|
||||||
String profileUUID = System.getProperty("launcher.runtime.profileuuid", null);
|
String profileUUID = System.getProperty("launcher.runtime.profileuuid", null);
|
||||||
String mainClass = System.getProperty("launcher.runtime.mainclass", null);
|
String mainClass = System.getProperty("launcher.runtime.mainclass", null);
|
||||||
|
String mainModule = System.getProperty("launcher.runtime.mainmodule", null);
|
||||||
|
String launchMode = System.getProperty("launcher.runtime.launch", "basic");
|
||||||
|
String compatClasses = System.getProperty("launcher.runtime.launch.compat", null);
|
||||||
|
String nativesDir = System.getProperty("launcher.runtime.launch.natives", "natives");
|
||||||
|
String launcherOptionsPath = System.getProperty("launcher.runtime.launch.options", null);
|
||||||
ClientPermissions permissions = new ClientPermissions();
|
ClientPermissions permissions = new ClientPermissions();
|
||||||
if(mainClass == null) {
|
if(mainClass == null) {
|
||||||
throw new NullPointerException("Add `-Dlauncher.runtime.mainclass=YOUR_MAIN_CLASS` to jvmArgs");
|
throw new NullPointerException("Add `-Dlauncher.runtime.mainclass=YOUR_MAIN_CLASS` to jvmArgs");
|
||||||
|
@ -79,8 +92,42 @@ public void run(String[] args) {
|
||||||
AuthService.uuid = UUID.fromString(uuid);
|
AuthService.uuid = UUID.fromString(uuid);
|
||||||
AuthService.username = username;
|
AuthService.username = username;
|
||||||
AuthService.permissions = permissions;
|
AuthService.permissions = permissions;
|
||||||
Class<?> mainClazz = Class.forName(mainClass);
|
Launch launch;
|
||||||
mainClazz.getMethod("main", String[].class).invoke(null, (Object) newArgs.toArray(new String[0]));
|
switch (launchMode) {
|
||||||
|
case "basic": {
|
||||||
|
launch = new BasicLaunch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "legacy": {
|
||||||
|
launch = new LegacyLaunch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "module": {
|
||||||
|
launch = new ModuleLaunch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new UnsupportedOperationException(String.format("Unknown launch mode: '%s'", launchMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Path> classpath = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for(var c : System.getProperty("java.class.path").split(File.pathSeparator)) {
|
||||||
|
classpath.add(Paths.get(c));
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LogHelper.error(e);
|
||||||
|
}
|
||||||
|
LaunchOptions options;
|
||||||
|
if(launcherOptionsPath != null) {
|
||||||
|
try(Reader reader = IOHelper.newReader(Paths.get(launcherOptionsPath))) {
|
||||||
|
options = Launcher.gsonManager.gson.fromJson(reader, LaunchOptions.class);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options = new LaunchOptions();
|
||||||
|
}
|
||||||
|
launch.init(classpath, nativesDir, options);
|
||||||
|
launch.launch(mainClass, mainModule, Arrays.asList(args));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LogHelper.error(e);
|
LogHelper.error(e);
|
||||||
LauncherEngine.exitLauncher(-15);
|
LauncherEngine.exitLauncher(-15);
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import pro.gravit.launcher.profiles.optional.triggers.OptionalTrigger;
|
import pro.gravit.launcher.profiles.optional.triggers.OptionalTrigger;
|
||||||
import pro.gravit.utils.helper.IOHelper;
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
import pro.gravit.utils.helper.VerifyHelper;
|
import pro.gravit.utils.helper.VerifyHelper;
|
||||||
|
import pro.gravit.utils.launch.LaunchOptions;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
@ -80,6 +81,10 @@ public final class ClientProfile implements Comparable<ClientProfile> {
|
||||||
// Client launcher
|
// Client launcher
|
||||||
@LauncherNetworkAPI
|
@LauncherNetworkAPI
|
||||||
private String mainClass;
|
private String mainClass;
|
||||||
|
@LauncherNetworkAPI
|
||||||
|
private String mainModule;
|
||||||
|
@LauncherNetworkAPI
|
||||||
|
private LaunchOptions.ModuleConf moduleConf;
|
||||||
|
|
||||||
public ClientProfile() {
|
public ClientProfile() {
|
||||||
update = new ArrayList<>();
|
update = new ArrayList<>();
|
||||||
|
@ -211,6 +216,14 @@ public String getMainClass() {
|
||||||
return mainClass;
|
return mainClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getMainModule() {
|
||||||
|
return mainModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchOptions.ModuleConf getModuleConf() {
|
||||||
|
return moduleConf;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ServerProfile> getServers() {
|
public List<ServerProfile> getServers() {
|
||||||
return servers;
|
return servers;
|
||||||
}
|
}
|
||||||
|
@ -447,7 +460,7 @@ public enum ClassLoaderConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum CompatibilityFlags {
|
public enum CompatibilityFlags {
|
||||||
LEGACY_NATIVES_DIR
|
LEGACY_NATIVES_DIR, CLASS_CONTROL_API
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Version implements Comparable<Version> {
|
public static class Version implements Comparable<Version> {
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
package pro.gravit.launcher.api;
|
package pro.gravit.launcher.api;
|
||||||
|
|
||||||
import pro.gravit.launcher.utils.ApiBridgeService;
|
import pro.gravit.launcher.utils.ApiBridgeService;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
import pro.gravit.utils.launch.ClassLoaderControl;
|
||||||
|
|
||||||
import java.lang.instrument.Instrumentation;
|
import java.lang.instrument.Instrumentation;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
public class ClientService {
|
public class ClientService {
|
||||||
public static Instrumentation instrumentation;
|
public static Instrumentation instrumentation;
|
||||||
public static ClassLoader classLoader;
|
public static ClassLoaderControl classLoaderControl;
|
||||||
public static String nativePath;
|
public static String nativePath;
|
||||||
public static URL[] baseURLs;
|
public static URL[] baseURLs;
|
||||||
|
|
||||||
public static ClassLoader getClassLoader() {
|
|
||||||
return classLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String findLibrary(String name) {
|
public static String findLibrary(String name) {
|
||||||
return ApiBridgeService.findLibrary(classLoader, name);
|
return nativePath.concat(IOHelper.PLATFORM_SEPARATOR).concat(JVMHelper.NATIVE_PREFIX).concat(name).concat(JVMHelper.NATIVE_EXTENSION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
package pro.gravit.launcher.client;
|
|
||||||
|
|
||||||
import pro.gravit.utils.helper.IOHelper;
|
|
||||||
import pro.gravit.utils.helper.JVMHelper;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLClassLoader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ClientClassLoader extends URLClassLoader {
|
|
||||||
private static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader();
|
|
||||||
public String nativePath;
|
|
||||||
|
|
||||||
private final List<String> packages = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new URLClassLoader for the specified URLs using the
|
|
||||||
* default delegation parent {@code ClassLoader}. The URLs will
|
|
||||||
* be searched in the order specified for classes and resources after
|
|
||||||
* first searching in the parent class loader. Any URL that ends with
|
|
||||||
* a '/' is assumed to refer to a directory. Otherwise, the URL is
|
|
||||||
* assumed to refer to a JAR file which will be downloaded and opened
|
|
||||||
* as needed.
|
|
||||||
*
|
|
||||||
* <p>If there is a security manager, this method first
|
|
||||||
* calls the security manager's {@code checkCreateClassLoader} method
|
|
||||||
* to ensure creation of a class loader is allowed.
|
|
||||||
*
|
|
||||||
* @param urls the URLs from which to load classes and resources
|
|
||||||
* @throws SecurityException if a security manager exists and its
|
|
||||||
* {@code checkCreateClassLoader} method doesn't allow
|
|
||||||
* creation of a class loader.
|
|
||||||
* @throws NullPointerException if {@code urls} is {@code null}.
|
|
||||||
*/
|
|
||||||
public ClientClassLoader(URL[] urls) {
|
|
||||||
super(urls);
|
|
||||||
packages.add("pro.gravit.launcher.");
|
|
||||||
packages.add("pro.gravit.utils.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new URLClassLoader for the given URLs. The URLs will be
|
|
||||||
* searched in the order specified for classes and resources after first
|
|
||||||
* searching in the specified parent class loader. Any {@code jar:}
|
|
||||||
* scheme URL is assumed to refer to a JAR file. Any {@code file:} scheme
|
|
||||||
* URL that ends with a '/' is assumed to refer to a directory. Otherwise,
|
|
||||||
* the URL is assumed to refer to a JAR file which will be downloaded and
|
|
||||||
* opened as needed.
|
|
||||||
*
|
|
||||||
* <p>If there is a security manager, this method first
|
|
||||||
* calls the security manager's {@code checkCreateClassLoader} method
|
|
||||||
* to ensure creation of a class loader is allowed.
|
|
||||||
*
|
|
||||||
* @param urls the URLs from which to load classes and resources
|
|
||||||
* @param parent the parent class loader for delegation
|
|
||||||
* @throws SecurityException if a security manager exists and its
|
|
||||||
* {@code checkCreateClassLoader} method doesn't allow
|
|
||||||
* creation of a class loader.
|
|
||||||
* @throws NullPointerException if {@code urls} is {@code null}.
|
|
||||||
*/
|
|
||||||
public ClientClassLoader(URL[] urls, ClassLoader parent) {
|
|
||||||
super(urls, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
|
||||||
if(name != null) {
|
|
||||||
for(String pkg : packages) {
|
|
||||||
if(name.startsWith(pkg)) {
|
|
||||||
return SYSTEM_CLASS_LOADER.loadClass(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.loadClass(name, resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String findLibrary(String name) {
|
|
||||||
return nativePath.concat(IOHelper.PLATFORM_SEPARATOR).concat(JVMHelper.NATIVE_PREFIX).concat(name).concat(JVMHelper.NATIVE_EXTENSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAllowedPackage(String pkg) {
|
|
||||||
packages.add(pkg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addURL(URL url) {
|
|
||||||
super.addURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import pro.gravit.launcher.serialize.HInput;
|
import pro.gravit.launcher.serialize.HInput;
|
||||||
import pro.gravit.launcher.utils.DirWatcher;
|
import pro.gravit.launcher.utils.DirWatcher;
|
||||||
import pro.gravit.utils.helper.*;
|
import pro.gravit.utils.helper.*;
|
||||||
|
import pro.gravit.utils.launch.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -39,10 +40,12 @@
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class ClientLauncherEntryPoint {
|
public class ClientLauncherEntryPoint {
|
||||||
private static ClassLoader classLoader;
|
|
||||||
public static ClientModuleManager modulesManager;
|
public static ClientModuleManager modulesManager;
|
||||||
public static ClientParams clientParams;
|
public static ClientParams clientParams;
|
||||||
|
|
||||||
|
private static Launch launch;
|
||||||
|
private static ClassLoaderControl classLoaderControl;
|
||||||
|
|
||||||
private static ClientParams readParams(SocketAddress address) throws IOException {
|
private static ClientParams readParams(SocketAddress address) throws IOException {
|
||||||
try (Socket socket = IOHelper.newSocket()) {
|
try (Socket socket = IOHelper.newSocket()) {
|
||||||
socket.connect(address);
|
socket.connect(address);
|
||||||
|
@ -102,7 +105,7 @@ public static void main(String[] args) throws Throwable {
|
||||||
LogHelper.debug("Verifying ClientLauncher sign and classpath");
|
LogHelper.debug("Verifying ClientLauncher sign and classpath");
|
||||||
List<Path> classpath = resolveClassPath(clientDir, params.actions, params.profile)
|
List<Path> classpath = resolveClassPath(clientDir, params.actions, params.profile)
|
||||||
.filter(x -> !profile.getModulePath().contains(clientDir.relativize(x).toString()))
|
.filter(x -> !profile.getModulePath().contains(clientDir.relativize(x).toString()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
List<URL> classpathURLs = classpath.stream().map(IOHelper::toURL).collect(Collectors.toList());
|
List<URL> classpathURLs = classpath.stream().map(IOHelper::toURL).collect(Collectors.toList());
|
||||||
// Start client with WatchService monitoring
|
// Start client with WatchService monitoring
|
||||||
RequestService service;
|
RequestService service;
|
||||||
|
@ -128,34 +131,39 @@ public static void main(String[] args) throws Throwable {
|
||||||
}
|
}
|
||||||
LogHelper.debug("Natives dir %s", params.nativesDir);
|
LogHelper.debug("Natives dir %s", params.nativesDir);
|
||||||
ClientProfile.ClassLoaderConfig classLoaderConfig = profile.getClassLoaderConfig();
|
ClientProfile.ClassLoaderConfig classLoaderConfig = profile.getClassLoaderConfig();
|
||||||
|
LaunchOptions options = new LaunchOptions();
|
||||||
|
options.moduleConf = profile.getModuleConf();
|
||||||
if (classLoaderConfig == ClientProfile.ClassLoaderConfig.LAUNCHER) {
|
if (classLoaderConfig == ClientProfile.ClassLoaderConfig.LAUNCHER) {
|
||||||
ClientClassLoader classLoader = new ClientClassLoader(classpathURLs.toArray(new URL[0]), ClientLauncherEntryPoint.class.getClassLoader());
|
if(JVMHelper.JVM_VERSION <= 11) {
|
||||||
|
launch = new LegacyLaunch();
|
||||||
|
} else {
|
||||||
|
launch = new ModuleLaunch();
|
||||||
|
}
|
||||||
|
classLoaderControl = launch.init(classpath, params.nativesDir, options);
|
||||||
System.setProperty("java.class.path", classpath.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator)));
|
System.setProperty("java.class.path", classpath.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator)));
|
||||||
ClientLauncherEntryPoint.classLoader = classLoader;
|
modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(launch, classLoaderControl, profile));
|
||||||
Thread.currentThread().setContextClassLoader(classLoader);
|
ClientService.nativePath = params.nativesDir;
|
||||||
classLoader.nativePath = params.nativesDir;
|
ClientService.baseURLs = classLoaderControl.getURLs();
|
||||||
modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(classLoader, profile));
|
|
||||||
ClientService.classLoader = classLoader;
|
|
||||||
ClientService.nativePath = classLoader.nativePath;
|
|
||||||
classLoader.addURL(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL());
|
|
||||||
ClientService.baseURLs = classLoader.getURLs();
|
|
||||||
} else if (classLoaderConfig == ClientProfile.ClassLoaderConfig.AGENT) {
|
} else if (classLoaderConfig == ClientProfile.ClassLoaderConfig.AGENT) {
|
||||||
ClientLauncherEntryPoint.classLoader = ClassLoader.getSystemClassLoader();
|
launch = new BasicLaunch(LauncherAgent.inst);
|
||||||
classpathURLs.add(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL());
|
classpathURLs.add(IOHelper.getCodeSource(ClientLauncherEntryPoint.class).toUri().toURL());
|
||||||
|
classLoaderControl = launch.init(classpath, params.nativesDir, options);
|
||||||
for (URL url : classpathURLs) {
|
for (URL url : classpathURLs) {
|
||||||
LauncherAgent.addJVMClassPath(Paths.get(url.toURI()));
|
LauncherAgent.addJVMClassPath(Paths.get(url.toURI()));
|
||||||
}
|
}
|
||||||
ClientService.instrumentation = LauncherAgent.inst;
|
ClientService.instrumentation = LauncherAgent.inst;
|
||||||
ClientService.nativePath = params.nativesDir;
|
ClientService.nativePath = params.nativesDir;
|
||||||
modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(classLoader, profile));
|
modulesManager.invokeEvent(new ClientProcessClassLoaderEvent(launch, null, profile));
|
||||||
ClientService.classLoader = classLoader;
|
|
||||||
ClientService.baseURLs = classpathURLs.toArray(new URL[0]);
|
ClientService.baseURLs = classpathURLs.toArray(new URL[0]);
|
||||||
} else if (classLoaderConfig == ClientProfile.ClassLoaderConfig.SYSTEM_ARGS) {
|
} else if (classLoaderConfig == ClientProfile.ClassLoaderConfig.SYSTEM_ARGS) {
|
||||||
ClientLauncherEntryPoint.classLoader = ClassLoader.getSystemClassLoader();
|
launch = new BasicLaunch();
|
||||||
ClientService.classLoader = ClassLoader.getSystemClassLoader();
|
classLoaderControl = launch.init(classpath, params.nativesDir, options);
|
||||||
ClientService.baseURLs = classpathURLs.toArray(new URL[0]);
|
ClientService.baseURLs = classpathURLs.toArray(new URL[0]);
|
||||||
ClientService.nativePath = params.nativesDir;
|
ClientService.nativePath = params.nativesDir;
|
||||||
}
|
}
|
||||||
|
if(profile.hasFlag(ClientProfile.CompatibilityFlags.CLASS_CONTROL_API)) {
|
||||||
|
ClientService.classLoaderControl = classLoaderControl;
|
||||||
|
}
|
||||||
AuthService.username = params.playerProfile.username;
|
AuthService.username = params.playerProfile.username;
|
||||||
AuthService.uuid = params.playerProfile.uuid;
|
AuthService.uuid = params.playerProfile.uuid;
|
||||||
KeyService.serverRsaPublicKey = Launcher.getConfig().rsaPublicKey;
|
KeyService.serverRsaPublicKey = Launcher.getConfig().rsaPublicKey;
|
||||||
|
@ -264,27 +272,20 @@ private static void launch(ClientProfile profile, ClientParams params) throws Th
|
||||||
}
|
}
|
||||||
LogHelper.debug("Args: " + copy);
|
LogHelper.debug("Args: " + copy);
|
||||||
// Resolve main class and method
|
// Resolve main class and method
|
||||||
Class<?> mainClass = classLoader.loadClass(profile.getMainClass());
|
|
||||||
if (LogHelper.isDevEnabled() && classLoader instanceof URLClassLoader) {
|
|
||||||
for (URL u : ((URLClassLoader) classLoader).getURLs()) {
|
|
||||||
LogHelper.dev("ClassLoader URL: %s", u.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modulesManager.invokeEvent(new ClientProcessPreInvokeMainClassEvent(params, profile, args));
|
modulesManager.invokeEvent(new ClientProcessPreInvokeMainClassEvent(params, profile, args));
|
||||||
// Invoke main method
|
// Invoke main method
|
||||||
try {
|
try {
|
||||||
{
|
{
|
||||||
List<String> compatClasses = profile.getCompatClasses();
|
List<String> compatClasses = profile.getCompatClasses();
|
||||||
for (String e : compatClasses) {
|
for (String e : compatClasses) {
|
||||||
Class<?> clazz = classLoader.loadClass(e);
|
Class<?> clazz = classLoaderControl.getClass(e);
|
||||||
MethodHandle runMethod = MethodHandles.lookup().findStatic(clazz, "run", MethodType.methodType(void.class));
|
MethodHandle runMethod = MethodHandles.lookup().findStatic(clazz, "run", MethodType.methodType(void.class, ClassLoaderControl.class));
|
||||||
runMethod.invoke();
|
runMethod.invoke(classLoaderControl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)).asFixedArity();
|
|
||||||
Launcher.LAUNCHED.set(true);
|
Launcher.LAUNCHED.set(true);
|
||||||
JVMHelper.fullGC();
|
JVMHelper.fullGC();
|
||||||
mainMethod.invokeWithArguments((Object) args.toArray(new String[0]));
|
launch.launch(params.profile.getMainClass(), params.profile.getMainModule(), args);
|
||||||
LogHelper.debug("Main exit successful");
|
LogHelper.debug("Main exit successful");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LogHelper.error(e);
|
LogHelper.error(e);
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
|
|
||||||
import pro.gravit.launcher.modules.LauncherModule;
|
import pro.gravit.launcher.modules.LauncherModule;
|
||||||
import pro.gravit.launcher.profiles.ClientProfile;
|
import pro.gravit.launcher.profiles.ClientProfile;
|
||||||
|
import pro.gravit.utils.launch.ClassLoaderControl;
|
||||||
|
import pro.gravit.utils.launch.Launch;
|
||||||
|
|
||||||
public class ClientProcessClassLoaderEvent extends LauncherModule.Event {
|
public class ClientProcessClassLoaderEvent extends LauncherModule.Event {
|
||||||
public final ClassLoader clientClassLoader;
|
public final Launch launch;
|
||||||
|
public final ClassLoaderControl classLoaderControl;
|
||||||
public final ClientProfile profile;
|
public final ClientProfile profile;
|
||||||
|
|
||||||
public ClientProcessClassLoaderEvent(ClassLoader clientClassLoader, ClientProfile profile) {
|
public ClientProcessClassLoaderEvent(Launch launch, ClassLoaderControl classLoaderControl, ClientProfile profile) {
|
||||||
this.clientClassLoader = clientClassLoader;
|
this.launch = launch;
|
||||||
|
this.classLoaderControl = classLoaderControl;
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import pro.gravit.launcher.Launcher;
|
import pro.gravit.launcher.Launcher;
|
||||||
import pro.gravit.launcher.LauncherTrustManager;
|
import pro.gravit.launcher.LauncherTrustManager;
|
||||||
import pro.gravit.launcher.client.ClientClassLoader;
|
|
||||||
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
@ -16,12 +15,4 @@ public static void checkCertificatesSuccess(X509Certificate[] certs) throws Exce
|
||||||
LauncherTrustManager trustManager = Launcher.getConfig().trustManager;
|
LauncherTrustManager trustManager = Launcher.getConfig().trustManager;
|
||||||
trustManager.checkCertificatesSuccess(certs, trustManager::stdCertificateChecker);
|
trustManager.checkCertificatesSuccess(certs, trustManager::stdCertificateChecker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String findLibrary(ClassLoader classLoader, String library) {
|
|
||||||
if (classLoader instanceof ClientClassLoader) {
|
|
||||||
ClientClassLoader clientClassLoader = (ClientClassLoader) classLoader;
|
|
||||||
return clientClassLoader.findLibrary(library);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.instrument.ClassFileTransformer;
|
||||||
|
import java.lang.instrument.IllegalClassFormatException;
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
public class BasicLaunch implements Launch {
|
||||||
|
|
||||||
|
private Instrumentation instrumentation;
|
||||||
|
|
||||||
|
public BasicLaunch(Instrumentation instrumentation) {
|
||||||
|
this.instrumentation = instrumentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasicLaunch() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoaderControl init(List<Path> files, String nativePath, LaunchOptions options) {
|
||||||
|
return new BasicClassLoaderControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launch(String mainClass, String mainModule, Collection<String> args) throws Throwable {
|
||||||
|
Class<?> mainClazz = Class.forName(mainClass);
|
||||||
|
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClazz, "main", MethodType.methodType(void.class, String[].class)).asFixedArity();
|
||||||
|
JVMHelper.fullGC();
|
||||||
|
mainMethod.invokeWithArguments((Object) args.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BasicClassLoaderControl implements ClassLoaderControl {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLauncherPackage(String prefix) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransformer(ClassTransformer transformer) {
|
||||||
|
if (instrumentation == null) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
instrumentation.addTransformer(new ClassFileTransformer() {
|
||||||
|
@Override
|
||||||
|
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
|
||||||
|
if(transformer.filter(null, className)) {
|
||||||
|
return transformer.transform(null, className, protectionDomain, classfileBuffer);
|
||||||
|
}
|
||||||
|
return classfileBuffer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addURL(URL url) {
|
||||||
|
if (instrumentation == null) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
instrumentation.appendToSystemClassLoaderSearch(new JarFile(new File(url.toURI())));
|
||||||
|
} catch (URISyntaxException | IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addJar(Path path) {
|
||||||
|
if (instrumentation == null) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
instrumentation.appendToSystemClassLoaderSearch(new JarFile(path.toFile()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL[] getURLs() {
|
||||||
|
String classpath = System.getProperty("java.class.path");
|
||||||
|
String[] split = classpath.split(File.pathSeparator);
|
||||||
|
URL[] urls = new URL[split.length];
|
||||||
|
try {
|
||||||
|
for(int i=0;i<split.length;i++) {
|
||||||
|
urls[i] = Paths.get(split[i]).toAbsolutePath().toUri().toURL();
|
||||||
|
}
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getClass(String name) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoader getClassLoader() {
|
||||||
|
return BasicLaunch.class.getClassLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getJava9ModuleController() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.ProtectionDomain;
|
||||||
|
|
||||||
|
public interface ClassLoaderControl {
|
||||||
|
void addLauncherPackage(String prefix);
|
||||||
|
void addTransformer(ClassTransformer transformer);
|
||||||
|
void addURL(URL url);
|
||||||
|
void addJar(Path path);
|
||||||
|
URL[] getURLs();
|
||||||
|
|
||||||
|
Class<?> getClass(String name) throws ClassNotFoundException;
|
||||||
|
|
||||||
|
ClassLoader getClassLoader();
|
||||||
|
|
||||||
|
Object getJava9ModuleController();
|
||||||
|
interface ClassTransformer {
|
||||||
|
boolean filter(String moduleName, String name);
|
||||||
|
byte[] transform(String moduleName, String name, ProtectionDomain protectionDomain, byte[] data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Launch {
|
||||||
|
ClassLoaderControl init(List<Path> files, String nativePath, LaunchOptions options);
|
||||||
|
void launch(String mainClass, String mainModule, Collection<String> args) throws Throwable;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class LaunchOptions {
|
||||||
|
public ModuleConf moduleConf;
|
||||||
|
|
||||||
|
|
||||||
|
public static final class ModuleConf {
|
||||||
|
public List<String> modules = new ArrayList<>();
|
||||||
|
public List<String> modulePath = new ArrayList<>();
|
||||||
|
public Map<String, String> exports = new HashMap<>();
|
||||||
|
public Map<String, String> opens = new HashMap<>();
|
||||||
|
public Map<String, String> reads = new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class LegacyLaunch implements Launch {
|
||||||
|
private LegacyClassLoader legacyClassLoader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoaderControl init(List<Path> files, String nativePath, LaunchOptions options) {
|
||||||
|
legacyClassLoader = new LegacyClassLoader(files.stream().map((e) -> {
|
||||||
|
try {
|
||||||
|
return e.toUri().toURL();
|
||||||
|
} catch (MalformedURLException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}).toArray(URL[]::new), BasicLaunch.class.getClassLoader());
|
||||||
|
legacyClassLoader.nativePath = nativePath;
|
||||||
|
return legacyClassLoader.makeControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launch(String mainClass, String mainModule, Collection<String> args) throws Throwable {
|
||||||
|
Thread.currentThread().setContextClassLoader(legacyClassLoader);
|
||||||
|
Class<?> mainClazz = Class.forName(mainClass, true, legacyClassLoader);
|
||||||
|
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClazz, "main", MethodType.methodType(void.class, String[].class)).asFixedArity();
|
||||||
|
JVMHelper.fullGC();
|
||||||
|
mainMethod.invokeWithArguments((Object) args.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LegacyClassLoader extends URLClassLoader {
|
||||||
|
private final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader();
|
||||||
|
private final List<ClassLoaderControl.ClassTransformer> transformers = new ArrayList<>();
|
||||||
|
private final Map<String, Class<?>> classMap = new ConcurrentHashMap<>();
|
||||||
|
private String nativePath;
|
||||||
|
|
||||||
|
private final List<String> packages = new ArrayList<>();
|
||||||
|
public LegacyClassLoader(URL[] urls) {
|
||||||
|
super(urls);
|
||||||
|
packages.add("pro.gravit.launcher.");
|
||||||
|
packages.add("pro.gravit.utils.");
|
||||||
|
}
|
||||||
|
public LegacyClassLoader(URL[] urls, ClassLoader parent) {
|
||||||
|
super(urls, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
if(name != null) {
|
||||||
|
for(String pkg : packages) {
|
||||||
|
if(name.startsWith(pkg)) {
|
||||||
|
return SYSTEM_CLASS_LOADER.loadClass(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
Class<?> clazz;
|
||||||
|
{
|
||||||
|
clazz = classMap.get(name);
|
||||||
|
if(clazz != null) {
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(name != null && !transformers.isEmpty()) {
|
||||||
|
boolean needTransform = false;
|
||||||
|
for(ClassLoaderControl.ClassTransformer t : transformers) {
|
||||||
|
if(t.filter(null, name)) {
|
||||||
|
needTransform = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(needTransform) {
|
||||||
|
String rawClassName = name.replace(".", "/").concat(".class");
|
||||||
|
try(InputStream input = getResourceAsStream(rawClassName)) {
|
||||||
|
byte[] bytes = IOHelper.read(input);
|
||||||
|
for(ClassLoaderControl.ClassTransformer t : transformers) {
|
||||||
|
bytes = t.transform(null, name, null, bytes);
|
||||||
|
}
|
||||||
|
clazz = defineClass(name, bytes, 0, bytes.length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClassNotFoundException(name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(clazz == null) {
|
||||||
|
clazz = super.findClass(name);
|
||||||
|
}
|
||||||
|
if(clazz != null) {
|
||||||
|
classMap.put(name, clazz);
|
||||||
|
return clazz;
|
||||||
|
} else {
|
||||||
|
throw new ClassNotFoundException(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String findLibrary(String name) {
|
||||||
|
return nativePath.concat(IOHelper.PLATFORM_SEPARATOR).concat(JVMHelper.NATIVE_PREFIX).concat(name).concat(JVMHelper.NATIVE_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllowedPackage(String pkg) {
|
||||||
|
packages.add(pkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LegacyClassLoaderControl makeControl() {
|
||||||
|
return new LegacyClassLoaderControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LegacyClassLoaderControl implements ClassLoaderControl {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLauncherPackage(String prefix) {
|
||||||
|
addAllowedPackage(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransformer(ClassTransformer transformer) {
|
||||||
|
transformers.add(transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addURL(URL url) {
|
||||||
|
LegacyClassLoader.this.addURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addJar(Path path) {
|
||||||
|
try {
|
||||||
|
LegacyClassLoader.this.addURL(path.toUri().toURL());
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL[] getURLs() {
|
||||||
|
return LegacyClassLoader.this.getURLs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getClass(String name) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name, false, LegacyClassLoader.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoader getClassLoader() {
|
||||||
|
return LegacyClassLoader.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getJava9ModuleController() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ModuleLaunch implements Launch {
|
||||||
|
@Override
|
||||||
|
public ClassLoaderControl init(List<Path> files, String nativePath, LaunchOptions options) {
|
||||||
|
throw new UnsupportedOperationException("Please use Multi-Release JAR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launch(String mainClass, String mainModule, Collection<String> args) throws Throwable {
|
||||||
|
throw new UnsupportedOperationException("Please use Multi-Release JAR");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
package pro.gravit.utils.launch;
|
||||||
|
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.lang.module.Configuration;
|
||||||
|
import java.lang.module.ModuleFinder;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class ModuleLaunch implements Launch {
|
||||||
|
private ModuleClassLoader moduleClassLoader;
|
||||||
|
private Configuration configuration;
|
||||||
|
private ModuleLayer.Controller controller;
|
||||||
|
private ModuleFinder moduleFinder;
|
||||||
|
private ModuleLayer layer;
|
||||||
|
@Override
|
||||||
|
public ClassLoaderControl init(List<Path> files, String nativePath, LaunchOptions options) {
|
||||||
|
moduleClassLoader = new ModuleClassLoader(files.stream().map((e) -> {
|
||||||
|
try {
|
||||||
|
return e.toUri().toURL();
|
||||||
|
} catch (MalformedURLException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}).toArray(URL[]::new), BasicLaunch.class.getClassLoader());
|
||||||
|
moduleClassLoader.nativePath = nativePath;
|
||||||
|
{
|
||||||
|
if(options.moduleConf != null) {
|
||||||
|
// Create Module Layer
|
||||||
|
moduleFinder = ModuleFinder.of(options.moduleConf.modulePath.stream().map(Paths::get).toArray(Path[]::new));
|
||||||
|
ModuleLayer bootLayer = ModuleLayer.boot();
|
||||||
|
configuration = bootLayer.configuration()
|
||||||
|
.resolveAndBind(ModuleFinder.of(), moduleFinder, options.moduleConf.modules);
|
||||||
|
controller = ModuleLayer.defineModulesWithOneLoader(configuration, List.of(bootLayer), moduleClassLoader);
|
||||||
|
moduleClassLoader.controller = controller;
|
||||||
|
layer = controller.layer();
|
||||||
|
// Configure exports / opens
|
||||||
|
for(var e : options.moduleConf.exports.entrySet()) {
|
||||||
|
String[] split = e.getKey().split("\\\\");
|
||||||
|
Module source = layer.findModule(split[0]).orElseThrow();
|
||||||
|
String pkg = split[1];
|
||||||
|
Module target = layer.findModule(e.getValue()).orElseThrow();
|
||||||
|
controller.addExports(source, pkg, target);
|
||||||
|
}
|
||||||
|
for(var e : options.moduleConf.opens.entrySet()) {
|
||||||
|
String[] split = e.getKey().split("\\\\");
|
||||||
|
Module source = layer.findModule(split[0]).orElseThrow();
|
||||||
|
String pkg = split[1];
|
||||||
|
Module target = layer.findModule(e.getValue()).orElseThrow();
|
||||||
|
controller.addOpens(source, pkg, target);
|
||||||
|
}
|
||||||
|
for(var e : options.moduleConf.reads.entrySet()) {
|
||||||
|
Module source = layer.findModule(e.getKey()).orElseThrow();
|
||||||
|
Module target = layer.findModule(e.getValue()).orElseThrow();
|
||||||
|
controller.addReads(source, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return moduleClassLoader.makeControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launch(String mainClass, String mainModuleName, Collection<String> args) throws Throwable {
|
||||||
|
Thread.currentThread().setContextClassLoader(moduleClassLoader);
|
||||||
|
if(mainModuleName == null) {
|
||||||
|
Class<?> mainClazz = Class.forName(mainClass, true, moduleClassLoader);
|
||||||
|
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClazz, "main", MethodType.methodType(void.class, String[].class)).asFixedArity();
|
||||||
|
JVMHelper.fullGC();
|
||||||
|
mainMethod.invokeWithArguments((Object) args.toArray(new String[0]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Module mainModule = layer.findModule(mainModuleName).orElseThrow();
|
||||||
|
Module unnamed = ModuleLaunch.class.getClassLoader().getUnnamedModule();
|
||||||
|
if(unnamed != null) {
|
||||||
|
controller.addOpens(mainModule, getPackageFromClass(mainClass), unnamed);
|
||||||
|
}
|
||||||
|
// Start main class
|
||||||
|
ClassLoader loader = mainModule.getClassLoader();
|
||||||
|
Class<?> mainClazz = Class.forName(mainClass, true, loader);
|
||||||
|
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClazz, "main", MethodType.methodType(void.class, String[].class));
|
||||||
|
mainMethod.invoke((Object) args.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPackageFromClass(String clazz) {
|
||||||
|
int index = clazz.lastIndexOf(".");
|
||||||
|
if(index >= 0) {
|
||||||
|
return clazz.substring(0, index);
|
||||||
|
}
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ModuleClassLoader extends URLClassLoader {
|
||||||
|
private final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader();
|
||||||
|
private final List<ClassLoaderControl.ClassTransformer> transformers = new ArrayList<>();
|
||||||
|
private final Map<String, Class<?>> classMap = new ConcurrentHashMap<>();
|
||||||
|
private String nativePath;
|
||||||
|
|
||||||
|
private ModuleLayer.Controller controller;
|
||||||
|
|
||||||
|
private final List<String> packages = new ArrayList<>();
|
||||||
|
public ModuleClassLoader(URL[] urls) {
|
||||||
|
super(urls);
|
||||||
|
packages.add("pro.gravit.launcher.");
|
||||||
|
packages.add("pro.gravit.utils.");
|
||||||
|
}
|
||||||
|
public ModuleClassLoader(URL[] urls, ClassLoader parent) {
|
||||||
|
super(urls, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
if(name != null) {
|
||||||
|
for(String pkg : packages) {
|
||||||
|
if(name.startsWith(pkg)) {
|
||||||
|
return SYSTEM_CLASS_LOADER.loadClass(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> findClass(String moduleName, String name) {
|
||||||
|
|
||||||
|
Class<?> clazz;
|
||||||
|
{
|
||||||
|
clazz = classMap.get(name);
|
||||||
|
if(clazz != null) {
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(name != null && !transformers.isEmpty()) {
|
||||||
|
boolean needTransform = false;
|
||||||
|
for(ClassLoaderControl.ClassTransformer t : transformers) {
|
||||||
|
if(t.filter(moduleName, name)) {
|
||||||
|
needTransform = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(needTransform) {
|
||||||
|
String rawClassName = name.replace(".", "/").concat(".class");
|
||||||
|
try(InputStream input = getResourceAsStream(rawClassName)) {
|
||||||
|
byte[] bytes = IOHelper.read(input);
|
||||||
|
for(ClassLoaderControl.ClassTransformer t : transformers) {
|
||||||
|
bytes = t.transform(moduleName, name, null, bytes);
|
||||||
|
}
|
||||||
|
clazz = defineClass(name, bytes, 0, bytes.length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(clazz == null) {
|
||||||
|
clazz = super.findClass(moduleName, name);
|
||||||
|
}
|
||||||
|
if(clazz != null) {
|
||||||
|
classMap.put(name, clazz);
|
||||||
|
return clazz;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String findLibrary(String name) {
|
||||||
|
return nativePath.concat(IOHelper.PLATFORM_SEPARATOR).concat(JVMHelper.NATIVE_PREFIX).concat(name).concat(JVMHelper.NATIVE_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllowedPackage(String pkg) {
|
||||||
|
packages.add(pkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleClassLoaderControl makeControl() {
|
||||||
|
return new ModuleClassLoaderControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ModuleClassLoaderControl implements ClassLoaderControl {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLauncherPackage(String prefix) {
|
||||||
|
addAllowedPackage(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTransformer(ClassTransformer transformer) {
|
||||||
|
transformers.add(transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addURL(URL url) {
|
||||||
|
ModuleClassLoader.this.addURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addJar(Path path) {
|
||||||
|
try {
|
||||||
|
ModuleClassLoader.this.addURL(path.toUri().toURL());
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL[] getURLs() {
|
||||||
|
return ModuleClassLoader.this.getURLs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getClass(String name) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name, false, ModuleClassLoader.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoader getClassLoader() {
|
||||||
|
return ModuleClassLoader.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getJava9ModuleController() {
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,25 +17,24 @@
|
||||||
import pro.gravit.launcher.request.update.ProfilesRequest;
|
import pro.gravit.launcher.request.update.ProfilesRequest;
|
||||||
import pro.gravit.launcher.request.websockets.StdWebSocketService;
|
import pro.gravit.launcher.request.websockets.StdWebSocketService;
|
||||||
import pro.gravit.launcher.server.authlib.InstallAuthlib;
|
import pro.gravit.launcher.server.authlib.InstallAuthlib;
|
||||||
import pro.gravit.launcher.server.launch.ClasspathLaunch;
|
|
||||||
import pro.gravit.launcher.server.launch.Launch;
|
|
||||||
import pro.gravit.launcher.server.launch.ModuleLaunch;
|
|
||||||
import pro.gravit.launcher.server.launch.SimpleLaunch;
|
|
||||||
import pro.gravit.launcher.server.setup.ServerWrapperSetup;
|
import pro.gravit.launcher.server.setup.ServerWrapperSetup;
|
||||||
import pro.gravit.utils.PublicURLClassLoader;
|
import pro.gravit.utils.PublicURLClassLoader;
|
||||||
import pro.gravit.utils.helper.IOHelper;
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
import pro.gravit.utils.helper.LogHelper;
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
import pro.gravit.utils.helper.SecurityHelper;
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
import pro.gravit.utils.launch.*;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ServerWrapper extends JsonConfigurable<ServerWrapper.Config> {
|
public class ServerWrapper extends JsonConfigurable<ServerWrapper.Config> {
|
||||||
public static final Path configFile = Paths.get(System.getProperty("serverwrapper.configFile", "ServerWrapperConfig.json"));
|
public static final Path configFile = Paths.get(System.getProperty("serverwrapper.configFile", "ServerWrapperConfig.json"));
|
||||||
public static final boolean disableSetup = Boolean.parseBoolean(System.getProperty("serverwrapper.disableSetup", "false"));
|
public static final boolean disableSetup = Boolean.parseBoolean(System.getProperty("serverwrapper.disableSetup", "false"));
|
||||||
public static ServerWrapper wrapper;
|
public static ServerWrapper wrapper;
|
||||||
|
public static ClassLoaderControl classLoaderControl;
|
||||||
public Config config;
|
public Config config;
|
||||||
public PublicURLClassLoader ucp;
|
public PublicURLClassLoader ucp;
|
||||||
public ClassLoader loader;
|
public ClassLoader loader;
|
||||||
|
@ -173,19 +172,26 @@ public void run(String... args) throws Throwable {
|
||||||
Launch launch;
|
Launch launch;
|
||||||
switch (config.classLoaderConfig) {
|
switch (config.classLoaderConfig) {
|
||||||
case LAUNCHER:
|
case LAUNCHER:
|
||||||
launch = new ClasspathLaunch();
|
launch = new LegacyLaunch();
|
||||||
break;
|
break;
|
||||||
case MODULE:
|
case MODULE:
|
||||||
launch = new ModuleLaunch();
|
launch = new ModuleLaunch();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
launch = new SimpleLaunch();
|
if(ServerAgent.isAgentStarted()) {
|
||||||
|
launch = new BasicLaunch(ServerAgent.inst);
|
||||||
|
} else {
|
||||||
|
launch = new BasicLaunch();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
LaunchOptions options = new LaunchOptions();
|
||||||
|
options.moduleConf = config.moduleConf;
|
||||||
|
classLoaderControl = launch.init(config.classpath.stream().map(Paths::get).collect(Collectors.toCollection(ArrayList::new)), config.nativesDir, options);
|
||||||
LogHelper.info("Start Minecraft Server");
|
LogHelper.info("Start Minecraft Server");
|
||||||
LogHelper.debug("Invoke main method %s with %s", classname, launch.getClass().getName());
|
LogHelper.debug("Invoke main method %s with %s", classname, launch.getClass().getName());
|
||||||
try {
|
try {
|
||||||
launch.run(classname, config, real_args);
|
launch.launch(config.mainclass, config.mainmodule, Arrays.asList(real_args));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LogHelper.error(e);
|
LogHelper.error(e);
|
||||||
System.exit(-1);
|
System.exit(-1);
|
||||||
|
@ -233,13 +239,15 @@ public static final class Config {
|
||||||
public ClientProfile.ClassLoaderConfig classLoaderConfig;
|
public ClientProfile.ClassLoaderConfig classLoaderConfig;
|
||||||
public String librariesDir;
|
public String librariesDir;
|
||||||
public String mainclass;
|
public String mainclass;
|
||||||
|
public String mainmodule;
|
||||||
|
public String nativesDir = "natives";
|
||||||
public List<String> args;
|
public List<String> args;
|
||||||
public String authId;
|
public String authId;
|
||||||
public AuthRequestEvent.OAuthRequestEvent oauth;
|
public AuthRequestEvent.OAuthRequestEvent oauth;
|
||||||
public long oauthExpireTime;
|
public long oauthExpireTime;
|
||||||
public Map<String, Request.ExtendedToken> extendedTokens;
|
public Map<String, Request.ExtendedToken> extendedTokens;
|
||||||
public LauncherConfig.LauncherEnvironment env;
|
public LauncherConfig.LauncherEnvironment env;
|
||||||
public ModuleConf moduleConf = new ModuleConf();
|
public LaunchOptions.ModuleConf moduleConf = new LaunchOptions.ModuleConf();
|
||||||
|
|
||||||
public byte[] encodedServerRsaPublicKey;
|
public byte[] encodedServerRsaPublicKey;
|
||||||
|
|
||||||
|
@ -247,13 +255,4 @@ public static final class Config {
|
||||||
|
|
||||||
public Map<String, String> properties;
|
public Map<String, String> properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ModuleConf {
|
|
||||||
public List<String> modules = new ArrayList<>();
|
|
||||||
public List<String> modulePath = new ArrayList<>();
|
|
||||||
public String mainModule = "";
|
|
||||||
public Map<String, String> exports = new HashMap<>();
|
|
||||||
public Map<String, String> opens = new HashMap<>();
|
|
||||||
public Map<String, String> reads = new HashMap<>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package pro.gravit.launcher.server.launch;
|
|
||||||
|
|
||||||
import pro.gravit.launcher.server.ServerWrapper;
|
|
||||||
import pro.gravit.utils.PublicURLClassLoader;
|
|
||||||
import pro.gravit.utils.helper.IOHelper;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
public class ClasspathLaunch implements Launch {
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("ConfusingArgumentToVarargsMethod")
|
|
||||||
public void run(String mainclass, ServerWrapper.Config config, String[] args) throws Throwable {
|
|
||||||
URL[] urls = config.classpath.stream().map(Paths::get).map(IOHelper::toURL).toArray(URL[]::new);
|
|
||||||
ClassLoader ucl = new PublicURLClassLoader(urls);
|
|
||||||
Class<?> mainClass = Class.forName(mainclass, true, ucl);
|
|
||||||
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
|
||||||
mainMethod.invoke(args);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package pro.gravit.launcher.server.launch;
|
|
||||||
|
|
||||||
import pro.gravit.launcher.server.ServerWrapper;
|
|
||||||
|
|
||||||
public interface Launch {
|
|
||||||
void run(String mainclass, ServerWrapper.Config config, String[] args) throws Throwable;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package pro.gravit.launcher.server.launch;
|
|
||||||
|
|
||||||
import pro.gravit.launcher.server.ServerWrapper;
|
|
||||||
|
|
||||||
public class ModuleLaunch implements Launch {
|
|
||||||
@Override
|
|
||||||
public void run(String mainclass, ServerWrapper.Config config, String[] args) {
|
|
||||||
throw new UnsupportedOperationException("Module system not supported");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package pro.gravit.launcher.server.launch;
|
|
||||||
|
|
||||||
import pro.gravit.launcher.server.ServerWrapper;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
|
|
||||||
public class SimpleLaunch implements Launch {
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("ConfusingArgumentToVarargsMethod")
|
|
||||||
public void run(String mainclass, ServerWrapper.Config config, String[] args) throws Throwable {
|
|
||||||
Class<?> mainClass = Class.forName(mainclass);
|
|
||||||
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
|
||||||
mainMethod.invoke(args);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package pro.gravit.launcher.server.launch;
|
|
||||||
|
|
||||||
import pro.gravit.launcher.server.ServerWrapper;
|
|
||||||
import pro.gravit.utils.PublicURLClassLoader;
|
|
||||||
import pro.gravit.utils.helper.IOHelper;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.lang.module.Configuration;
|
|
||||||
import java.lang.module.ModuleFinder;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ModuleLaunch implements Launch {
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("ConfusingArgumentToVarargsMethod")
|
|
||||||
public void run(String mainclass, ServerWrapper.Config config, String[] args) throws Throwable {
|
|
||||||
URL[] urls = config.classpath.stream().map(Paths::get).map(IOHelper::toURL).toArray(URL[]::new);
|
|
||||||
ClassLoader ucl = new PublicURLClassLoader(urls);
|
|
||||||
// Create Module Layer
|
|
||||||
ModuleFinder finder = ModuleFinder.of(config.moduleConf.modulePath.stream().map(Paths::get).toArray(Path[]::new));
|
|
||||||
ModuleLayer bootLayer = ModuleLayer.boot();
|
|
||||||
Configuration configuration = bootLayer.configuration()
|
|
||||||
.resolveAndBind(ModuleFinder.of(), finder, config.moduleConf.modules);
|
|
||||||
ModuleLayer.Controller controller = ModuleLayer.defineModulesWithOneLoader(configuration, List.of(bootLayer), ucl);
|
|
||||||
ModuleLayer layer = controller.layer();
|
|
||||||
// Configure exports / opens
|
|
||||||
for(var e : config.moduleConf.exports.entrySet()) {
|
|
||||||
String[] split = e.getKey().split("\\\\");
|
|
||||||
Module source = layer.findModule(split[0]).orElseThrow();
|
|
||||||
String pkg = split[1];
|
|
||||||
Module target = layer.findModule(e.getValue()).orElseThrow();
|
|
||||||
controller.addExports(source, pkg, target);
|
|
||||||
}
|
|
||||||
for(var e : config.moduleConf.opens.entrySet()) {
|
|
||||||
String[] split = e.getKey().split("\\\\");
|
|
||||||
Module source = layer.findModule(split[0]).orElseThrow();
|
|
||||||
String pkg = split[1];
|
|
||||||
Module target = layer.findModule(e.getValue()).orElseThrow();
|
|
||||||
controller.addOpens(source, pkg, target);
|
|
||||||
}
|
|
||||||
for(var e : config.moduleConf.reads.entrySet()) {
|
|
||||||
Module source = layer.findModule(e.getKey()).orElseThrow();
|
|
||||||
Module target = layer.findModule(e.getValue()).orElseThrow();
|
|
||||||
controller.addReads(source, target);
|
|
||||||
}
|
|
||||||
Module mainModule = layer.findModule(config.moduleConf.mainModule).orElseThrow();
|
|
||||||
Module unnamed = ModuleLaunch.class.getClassLoader().getUnnamedModule();
|
|
||||||
if(unnamed != null) {
|
|
||||||
controller.addOpens(mainModule, getPackageFromClass(config.mainclass), unnamed);
|
|
||||||
}
|
|
||||||
// Start main class
|
|
||||||
ClassLoader loader = mainModule.getClassLoader();
|
|
||||||
Class<?> mainClass = Class.forName(mainclass, true, loader);
|
|
||||||
MethodHandle mainMethod = MethodHandles.lookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
|
||||||
mainMethod.invoke(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getPackageFromClass(String clazz) {
|
|
||||||
int index = clazz.lastIndexOf(".");
|
|
||||||
if(index >= 0) {
|
|
||||||
return clazz.substring(0, index);
|
|
||||||
}
|
|
||||||
return clazz;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@
|
||||||
id 'org.openjfx.javafxplugin' version '0.0.10' apply false
|
id 'org.openjfx.javafxplugin' version '0.0.10' apply false
|
||||||
}
|
}
|
||||||
group = 'pro.gravit.launcher'
|
group = 'pro.gravit.launcher'
|
||||||
version = '5.5.0'
|
version = '5.5.1-SNAPSHOT'
|
||||||
|
|
||||||
apply from: 'props.gradle'
|
apply from: 'props.gradle'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue