async client/config load, class preload, splash screen invokelater

This commit is contained in:
Lucwousin
2019-10-15 23:58:10 +02:00
parent 1282880a2c
commit 68d67435bf
9 changed files with 238 additions and 113 deletions

View File

@@ -30,6 +30,8 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import io.reactivex.Completable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
@@ -61,6 +63,7 @@ import net.runelite.client.game.chatbox.ChatboxPanelManager;
import net.runelite.client.graphics.ModelOutlineRenderer;
import net.runelite.client.menus.MenuManager;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.rs.ClientLoader;
import net.runelite.client.rs.ClientUpdateCheckMode;
import net.runelite.client.task.Scheduler;
import net.runelite.client.ui.ClientUI;
@@ -85,8 +88,8 @@ public class RuneLite
public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins");
public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots");
public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs");
public static boolean allowPrivateServer = false;
public static final Locale SYSTEM_LOCALE = Locale.getDefault();
public static boolean allowPrivateServer = false;
@Getter
private static Injector injector;
@@ -198,16 +201,30 @@ public class RuneLite
parser.accepts("help", "Show this text").forHelp();
OptionSet options = parser.parse(args);
if (options.has("help"))
{
parser.printHelpOn(System.out);
System.exit(0);
}
if (options.has("debug"))
{
final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
logger.setLevel(Level.DEBUG);
}
if (options.has("bootstrap"))
{
Bootstrapper.main(false);
System.exit(0);
}
if (options.has("bootstrap-staging"))
{
Bootstrapper.main(true);
System.exit(0);
}
if (options.has("proxy"))
{
String[] proxy = options.valueOf(proxyInfo).split(":");
@@ -238,10 +255,18 @@ public class RuneLite
}
}
if (options.has("help"))
final ClientLoader clientLoader = new ClientLoader(options.valueOf(updateMode));
Completable.fromAction(clientLoader::get)
.subscribeOn(Schedulers.single())
.subscribe();
Completable.fromAction(ClassPreloader::preload)
.subscribeOn(Schedulers.computation())
.subscribe();
if (!options.has("no-splash"))
{
parser.printHelpOn(System.out);
System.exit(0);
RuneLiteSplashScreen.init();
}
final boolean developerMode = options.has("developer-mode");
@@ -252,27 +277,13 @@ public class RuneLite
assert assertions = true;
if (!assertions)
{
java.util.logging.Logger.getAnonymousLogger().warning("Developers should enable assertions; Add `-ea` to your JVM arguments`");
log.warn("Developers should enable assertions; Add `-ea` to your JVM arguments`");
}
}
if (!options.has("no-splash"))
{
RuneLiteSplashScreen.init();
}
RuneLiteSplashScreen.stage(0, "Initializing client");
PROFILES_DIR.mkdirs();
if (options.has("debug"))
{
final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
logger.setLevel(Level.DEBUG);
}
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) ->
{
log.error("Uncaught exception:", throwable);
if (throwable instanceof AbstractMethodError)
{
RuneLiteSplashScreen.setError("Out of date!", "Classes are out of date; Build with Gradle again.");
@@ -284,13 +295,16 @@ public class RuneLite
RuneLiteSplashScreen.stage(0, "Starting OpenOSRS injector");
PROFILES_DIR.mkdirs();
final long start = System.currentTimeMillis();
injector = Guice.createInjector(new RuneLiteModule(
options.valueOf(updateMode),
clientLoader,
true));
injector.getInstance(RuneLite.class).start();
final long end = System.currentTimeMillis();
final RuntimeMXBean rb = ManagementFactory.getRuntimeMXBean();
final long uptime = rb.getUptime();

View File

@@ -31,6 +31,7 @@ import java.applet.Applet;
import java.io.File;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import net.runelite.api.Client;
@@ -46,8 +47,6 @@ import net.runelite.client.eventbus.EventBus;
import net.runelite.client.game.ItemManager;
import net.runelite.client.menus.MenuManager;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.rs.ClientLoader;
import net.runelite.client.rs.ClientUpdateCheckMode;
import net.runelite.client.task.Scheduler;
import net.runelite.client.util.DeferredEventBus;
import net.runelite.client.util.ExecutorServiceExceptionLogger;
@@ -61,19 +60,18 @@ public class RuneLiteModule extends AbstractModule
{
private static final int MAX_OKHTTP_CACHE_SIZE = 20 * 1024 * 1024; // 20mb
private final ClientUpdateCheckMode updateCheckMode;
private final Supplier<Applet> clientLoader;
private final boolean developerMode;
public RuneLiteModule(final ClientUpdateCheckMode updateCheckMode, final boolean developerMode)
public RuneLiteModule(final Supplier<Applet> clientLoader, boolean developerMode)
{
this.updateCheckMode = updateCheckMode;
this.clientLoader = clientLoader;
this.developerMode = developerMode;
}
@Override
protected void configure()
{
bindConstant().annotatedWith(Names.named("updateCheckMode")).to(updateCheckMode);
bindConstant().annotatedWith(Names.named("developerMode")).to(developerMode);
bind(ScheduledExecutorService.class).toInstance(new ExecutorServiceExceptionLogger(Executors.newSingleThreadScheduledExecutor()));
bind(OkHttpClient.class).toInstance(RuneLiteAPI.CLIENT.newBuilder()
@@ -102,9 +100,9 @@ public class RuneLiteModule extends AbstractModule
@Provides
@Singleton
Applet provideApplet(ClientLoader clientLoader)
Applet provideApplet()
{
return clientLoader.load();
return clientLoader.get();
}
@Provides

View File

@@ -52,10 +52,26 @@ public class EventBus implements EventBusInterface
Disposable disposable = getSubject(eventClass)
.filter(Objects::nonNull) // Filter out null objects, better safe than sorry
.cast(eventClass) // Cast it for easier usage
.subscribe(action, error ->
{
log.error("Error in eventbus", error);
});
.subscribe(action, error -> log.error("Error in eventbus", error));
getCompositeDisposable(lifecycle).add(disposable);
subscriptionList.put(lifecycle, eventClass);
}
@Override
public <T> void subscribe(Class<T> eventClass, @NonNull Object lifecycle, @NonNull Consumer<T> action, int takeUntil)
{
if (subscriptionList.containsKey(lifecycle) && eventClass.equals(subscriptionList.get(lifecycle)))
{
return;
}
Disposable disposable = getSubject(eventClass)
.filter(Objects::nonNull) // Filter out null objects, better safe than sorry
.cast(eventClass) // Cast it for easier usage
.take(takeUntil)
.doFinally(() -> unregister(lifecycle))
.subscribe(action, error -> log.error("Error in eventbus", error));
getCompositeDisposable(lifecycle).add(disposable);
subscriptionList.put(lifecycle, eventClass);

View File

@@ -25,68 +25,92 @@
*/
package net.runelite.client.rs;
import io.reactivex.Single;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import net.runelite.http.api.RuneLiteAPI;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
class ClientConfigLoader
{
public ClientConfigLoader()
private ClientConfigLoader()
{
throw new RuntimeException();
}
private static final String CONFIG_URL = "http://oldschool.runescape.com/jav_config.ws";
private static final int MAX_ATTEMPTS = 16;
static RSConfig fetch() throws IOException
static Single<RSConfig> fetch()
{
final Request request = new Request.Builder()
.url(CONFIG_URL)
.build();
final RSConfig config = new RSConfig();
try (final Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
return Single.create(obs ->
{
if (!response.isSuccessful())
int attempt = 0;
HostSupplier supplier = null;
HttpUrl url = HttpUrl.parse(CONFIG_URL);
final RSConfig config = new RSConfig();
while (attempt++ < MAX_ATTEMPTS)
{
throw new IOException("Unsuccessful response: " + response.message());
}
final Request request = new Request.Builder()
.url(url)
.build();
String str;
final BufferedReader in = new BufferedReader(new InputStreamReader(response.body().byteStream()));
while ((str = in.readLine()) != null)
{
int idx = str.indexOf('=');
if (idx == -1)
try (final Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{
continue;
}
if (!response.isSuccessful())
{
if (supplier == null)
{
supplier = new HostSupplier();
}
String s = str.substring(0, idx);
url = supplier.get();
continue;
}
switch (s)
{
case "param":
str = str.substring(idx + 1);
idx = str.indexOf('=');
s = str.substring(0, idx);
String str;
final BufferedReader in = new BufferedReader(new InputStreamReader(response.body().byteStream()));
while ((str = in.readLine()) != null)
{
int idx = str.indexOf('=');
config.getAppletProperties().put(s, str.substring(idx + 1));
break;
case "msg":
// ignore
break;
default:
config.getClassLoaderProperties().put(s, str.substring(idx + 1));
break;
if (idx == -1)
{
continue;
}
String s = str.substring(0, idx);
switch (s)
{
case "param":
str = str.substring(idx + 1);
idx = str.indexOf('=');
s = str.substring(0, idx);
config.getAppletProperties().put(s, str.substring(idx + 1));
break;
case "msg":
// ignore
break;
default:
config.getClassLoaderProperties().put(s, str.substring(idx + 1));
break;
}
}
obs.onSuccess(config);
return;
}
}
}
return config;
obs.onError(new IOException("Too many attempts"));
});
}
}

View File

@@ -32,32 +32,44 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.RuneLite;
import net.runelite.client.ui.RuneLiteSplashScreen;
@Slf4j
@Singleton
public class ClientLoader
public class ClientLoader implements Supplier<Applet>
{
private final ClientUpdateCheckMode updateCheckMode;
private ClientUpdateCheckMode updateCheckMode;
private Object client = null;
@Inject
private ClientLoader(
@Named("updateCheckMode") final ClientUpdateCheckMode updateCheckMode)
public ClientLoader(ClientUpdateCheckMode updateCheckMode)
{
this.updateCheckMode = updateCheckMode;
}
public Applet load()
@Override
public synchronized Applet get()
{
if (client == null)
{
client = doLoad();
}
if (client instanceof Throwable)
{
throw new RuntimeException((Throwable) client);
}
return (Applet) client;
}
private Object doLoad()
{
try
{
RuneLiteSplashScreen.stage(.2, "Fetching applet viewer config");
final RSConfig config = ClientConfigLoader.fetch();
final RSConfig config = ClientConfigLoader.fetch().blockingGet();
switch (updateCheckMode)
{
@@ -92,7 +104,7 @@ public class ClientLoader
private static Applet loadRLPlus(final RSConfig config)
throws ClassNotFoundException, InstantiationException, IllegalAccessException
{
RuneLiteSplashScreen.stage(.465, "Starting Old School RuneScape");
RuneLiteSplashScreen.stage(.465, "Starting Open Old School RuneScape");
ClassLoader rsClassLoader = new ClassLoader(ClientLoader.class.getClassLoader())
{
@@ -126,7 +138,7 @@ public class ClientLoader
private static Applet loadVanilla(final RSConfig config)
throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException
{
RuneLiteSplashScreen.stage(.465, "Starting Old School RuneScape");
RuneLiteSplashScreen.stage(.465, "Starting Vanilla Old School RuneScape");
final String codebase = config.getCodeBase();
final String initialJar = config.getInitialJar();

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2019 Abex
* Copyright (c) 2019, Lucas <https://github.com/lucwousin>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.rs;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import net.runelite.http.api.RuneLiteAPI;
import net.runelite.http.api.worlds.World;
import net.runelite.http.api.worlds.WorldClient;
import net.runelite.http.api.worlds.WorldResult;
import okhttp3.HttpUrl;
@Slf4j
class HostSupplier implements Supplier<HttpUrl>
{
private final Random random = new Random();
private Queue<HttpUrl> hosts = new ArrayDeque<>();
@Override
public HttpUrl get()
{
if (!hosts.isEmpty())
{
return hosts.poll();
}
List<HttpUrl> newHosts = new WorldClient(RuneLiteAPI.CLIENT)
.lookupWorlds()
.map(WorldResult::getWorlds)
.blockingSingle()
.stream()
.map(World::getAddress)
.map(HttpUrl::parse)
.collect(Collectors.toList());
Collections.shuffle(newHosts, random);
hosts.addAll(newHosts.subList(0, 16));
while (hosts.size() < 2)
{
hosts.add(HttpUrl.parse("oldschool" + (random.nextInt(50) + 1) + ".runescape.COM"));
}
return hosts.poll();
}
}

View File

@@ -26,7 +26,6 @@ package net.runelite.client.ui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
@@ -111,29 +110,17 @@ public class RuneLiteSplashScreen extends JFrame
public static void init()
{
try
SwingUtilities.invokeLater(() ->
{
SwingUtilities.invokeAndWait(() ->
try
{
if (INSTANCE != null)
{
return;
}
try
{
INSTANCE = new RuneLiteSplashScreen();
}
catch (Exception e)
{
log.warn("Unable to start splash screen", e);
}
});
}
catch (InterruptedException | InvocationTargetException bs)
{
throw new RuntimeException(bs);
}
INSTANCE = new RuneLiteSplashScreen();
}
catch (Exception e)
{
log.warn("Unable to start splash screen", e);
}
});
}
public static void close()

View File

@@ -57,8 +57,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import net.runelite.client.rs.ClientUpdateCheckMode;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class PluginManagerTest
@@ -83,7 +82,7 @@ public class PluginManagerTest
public void before() throws IOException
{
Injector injector = Guice.createInjector(Modules
.override(new RuneLiteModule(ClientUpdateCheckMode.NONE, true))
.override(new RuneLiteModule(() -> null, true))
.with(BoundFieldModule.of(this)));
RuneLite.setInjector(injector);
@@ -141,7 +140,7 @@ public class PluginManagerTest
{
List<Module> modules = new ArrayList<>();
modules.add(new GraphvizModule());
modules.add(new RuneLiteModule(ClientUpdateCheckMode.NONE, true));
modules.add(new RuneLiteModule(() -> null, true));
PluginManager pluginManager = new PluginManager(true, null, null, null, null, null);
pluginManager.loadCorePlugins();

View File

@@ -25,7 +25,6 @@
package net.runelite.client.rs;
import java.io.IOException;
import org.junit.Test;
/**
@@ -35,9 +34,9 @@ import org.junit.Test;
public class ClientConfigLoaderTest
{
@Test
public void test() throws IOException
public void test()
{
final RSConfig config = ClientConfigLoader.fetch();
final RSConfig config = ClientConfigLoader.fetch().blockingGet();
for (String key : config.getClassLoaderProperties().keySet())
{