From 2f1c489badcf3af92ae734b8c2f4e5c7e4ede06b Mon Sep 17 00:00:00 2001 From: Corey Date: Sun, 24 Nov 2024 21:42:33 +0000 Subject: [PATCH] fix: launching runelite via linux AppImage launcher --- build.gradle.kts | 5 +- gradle/libs.versions.toml | 2 + launcher/build.gradle.kts | 1 + .../kotlin/net/rsprox/launcher/Launcher.kt | 117 +++++++++++++++--- .../kotlin/net/rsprox/proxy/ProxyService.kt | 28 +---- .../rsprox/proxy/runelite/RuneliteLauncher.kt | 22 ++++ 6 files changed, 131 insertions(+), 44 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c85ba6fc..ccc3c3af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper import java.security.MessageDigest import kotlin.io.path.fileSize +val mainClass = "net.rsprox.gui.ProxyToolGuiKt" val s3Bucket = "cdn.rsprox.net" plugins { @@ -158,7 +159,7 @@ tasks.register("uploadJarsToS3") { } val bootstrap = Bootstrap( - proxy = Proxy(version = project.version.toString(), mainClass = "net.rsprox.gui.ProxyToolGuiKt"), + proxy = Proxy(version = project.version.toString(), mainClass = mainClass), artifacts = artifacts.sortedBy { it.name }, ) @@ -171,7 +172,7 @@ tasks.create("proxy") { environment("APP_VERSION", project.version) group = "run" classpath = sourceSets["main"].runtimeClasspath - mainClass.set("net.rsprox.gui.ProxyToolGuiKt") + mainClass.set(mainClass) } tasks.create("download") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10049905..77374dcd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ junixsocket = "2.10.0" gson = "2.11.0" aws-sdk-kotlin = "1.3.13" jaxb-api = "2.3.1" +jopt-simple = "5.0.1" [libraries] netty-bom = { module = "io.netty:netty-bom", version.ref = "netty" } @@ -57,6 +58,7 @@ zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } junixsocket = { module = "com.kohlschutter.junixsocket:junixsocket-core", version.ref = "junixsocket"} aws-sdk-kotlin-s3 = { module = "aws.sdk.kotlin:s3", version.ref = "aws-sdk-kotlin" } jaxb-api = { module = "javax.xml.bind:jaxb-api", version.ref = "jaxb-api" } +jopt-simple = { module = "net.sf.jopt-simple:jopt-simple", version.ref = "jopt-simple" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/launcher/build.gradle.kts b/launcher/build.gradle.kts index 398bec84..a013da0b 100644 --- a/launcher/build.gradle.kts +++ b/launcher/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.inline.logger) implementation(libs.okhttp3) implementation(libs.gson) + implementation(libs.jopt.simple) } tasks.withType { diff --git a/launcher/src/main/kotlin/net/rsprox/launcher/Launcher.kt b/launcher/src/main/kotlin/net/rsprox/launcher/Launcher.kt index 064ba714..6337f97d 100644 --- a/launcher/src/main/kotlin/net/rsprox/launcher/Launcher.kt +++ b/launcher/src/main/kotlin/net/rsprox/launcher/Launcher.kt @@ -2,14 +2,12 @@ package net.rsprox.launcher import com.github.michaelbull.logging.InlineLogger import com.google.gson.Gson +import joptsimple.OptionParser import net.rsprox.gui.SplashScreen import okhttp3.OkHttpClient import okhttp3.Request -import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStreamReader +import java.io.* +import java.net.URLClassLoader import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -17,15 +15,74 @@ import java.security.MessageDigest import java.security.Signature import java.security.cert.Certificate import java.security.cert.CertificateFactory -import java.util.Locale +import java.util.* +import java.util.stream.Collectors +import javax.swing.UIManager import kotlin.io.path.Path import kotlin.io.path.absolutePathString public fun main(args: Array) { + val logger = InlineLogger() + val parser = OptionParser(false) + parser.allowsUnrecognizedOptions() + parser.accepts("runelite", "Whether we are launching the RuneLite client") + parser.accepts("classpath", "Classpath for the process are are launching").withRequiredArg() + + val options = parser.parse(*args) + if (options.has("classpath")) { + // Sometimes we will be getting launched as a process by an existing RSProx process or a new runelite + // process, e.g. from a Linux AppImage launcher. In this case we need to load the classpath and launch + // the main class ourselves using reflection, passing along any necessary arguments. + + val loadingRunelite = options.has("runelite") + val classpathOpt = options.valueOf("classpath").toString() + val classpath = classpathOpt.split(File.pathSeparator).stream().map { + if (loadingRunelite) { + // runelite-launcher doesn't pass the fully qualified paths of jars, so construct them ourselves + Paths.get(System.getProperty("user.home"), ".runelite", "repository2", it).toFile() + } else { + Paths.get(it).toFile() + } + }.collect(Collectors.toList()) + + val jarUrls = classpath.map { it.toURI().toURL() }.toTypedArray() + val parent = ClassLoader.getPlatformClassLoader() + val loader = URLClassLoader(jarUrls, parent) + + UIManager.put("ClassLoader", loader) + val thread = Thread { + try { + val mainClassPath = if (loadingRunelite) "net.runelite.client.RuneLite" else listOf("net" + + ".runelite.launcher.Launcher", "net.rsprox.gui.ProxyToolGuiKt").first { it in args} + val mainClass = loader.loadClass(mainClassPath) + val mainArgs = if (loadingRunelite) { + // RuneLite doesn't allow unrecognised arguments so only use arguments after --classpath + args.copyOfRange(args.indexOfFirst { it == "--classpath" } + 2, args.size) + }else { + args.copyOfRange(args.indexOfFirst { it == mainClass.name } + 1, args.size) + } + + logger.info { "Launching process using reflection: $mainClassPath ${mainArgs.joinToString(", ")}" } + + val main = mainClass.getMethod("main", Array::class.java) + main.invoke(null, mainArgs) + } catch (ex: Exception) { + ex.printStackTrace() + } + } + thread.name = "RSProx" + thread.start() + + return + } + Locale.setDefault(Locale.US) SplashScreen.init() SplashScreen.stage(0.0, "Preparing", "Setting up environment") - val launcher = Launcher() + + val launcher = Launcher(args) + val launcherArgs = launcher.getLaunchArgs(args) + logger.info { "Running process: ${launcherArgs.joinToString(" ")}" } val builder = ProcessBuilder() @@ -53,11 +110,23 @@ public data class Bootstrap( val proxy: Proxy, ) -public class Launcher { +public class Launcher(args: Array) { private val bootstrap = getBootstrap() init { logger.info { "Initialising RSProx launcher ${bootstrap.proxy.version}" } + logger.info { + "OS name: ${System.getProperty("os.name")}, version: ${System.getProperty("os.version")}, arch: ${ + System.getProperty( + "os.arch" + ) + }" + } + logger.info { "Java path: ${getJava()}" } + logger.info { "Java version: ${System.getProperty("java.version")}" } + logger.info { "Launcher args: ${args.joinToString(",")}" } + logger.info { "AppImage: ${System.getenv("APPIMAGE")}" } + SplashScreen.stage(0.30, "Preparing", "Creating directories") Files.createDirectories(artifactRepo) } @@ -66,7 +135,7 @@ public class Launcher { val javaHome = Paths.get(System.getProperty("java.home")) if (!Files.exists(javaHome)) { - throw FileNotFoundException("JAVA_HOME is not set correctly! directory \"$javaHome\" does not exist.") + throw FileNotFoundException("JAVA_HOME is not set correctly! directory '$javaHome' does not exist.") } var javaPath = Paths.get(javaHome.toString(), "bin", "java.exe") @@ -76,7 +145,7 @@ public class Launcher { } if (!Files.exists(javaPath)) { - throw FileNotFoundException("java executable not found in directory \"" + javaPath.parent + "\"") + throw FileNotFoundException("java executable not found in directory '${javaPath.parent}'") } return javaPath.toAbsolutePath().toString() @@ -94,13 +163,27 @@ public class Launcher { classpath.append(artifactRepo.resolve(artifact.name).absolutePathString()) } - return listOf( - getJava(), - "-cp", - classpath.toString(), - bootstrap.proxy.mainClass, - *launcherArgs - ) + if (System.getenv("APPIMAGE") != null) { + return listOf( + System.getenv("APPIMAGE"), + "-c", + "-J", + "-XX:+DisableAttachMechanism", + "--", + "--classpath", + classpath.toString(), + bootstrap.proxy.mainClass, + *launcherArgs + ) + } else { + return listOf( + getJava(), + "-cp", + classpath.toString(), + bootstrap.proxy.mainClass, + *launcherArgs + ) + } } private fun download() { diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt index fa3f515e..d6e6afcd 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/ProxyService.kt @@ -54,14 +54,12 @@ import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters import org.newsclub.net.unix.AFUNIXServerSocket import org.newsclub.net.unix.AFUNIXSocketAddress import java.io.File -import java.io.FileNotFoundException import java.io.IOException import java.math.BigInteger import java.net.URL import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Path -import java.nio.file.Paths import java.util.* import java.util.concurrent.TimeUnit import java.util.stream.Collectors @@ -378,26 +376,6 @@ public class ProxyService( } } - private fun getJava(): String { - val javaHome = Paths.get(System.getProperty("java.home")) - - if (!Files.exists(javaHome)) { - throw FileNotFoundException("JAVA_HOME is not set correctly! directory \"$javaHome\" does not exist.") - } - - var javaPath = Paths.get(javaHome.toString(), "bin", "java.exe") - - if (!Files.exists(javaPath)) { - javaPath = Paths.get(javaHome.toString(), "bin", "java") - } - - if (!Files.exists(javaPath)) { - throw FileNotFoundException("java executable not found in directory \"" + javaPath.parent + "\"") - } - - return javaPath.toAbsolutePath().toString() - } - public fun launchRuneLiteClient( sessionMonitor: SessionMonitor, character: JagexCharacter?, @@ -411,7 +389,7 @@ public class ProxyService( } this.connections.addSessionMonitor(port, sessionMonitor) ClientTypeDictionary[port] = "RuneLite (${operatingSystem.shortName})" - launchJar( + launchJavaProcess( port, operatingSystem, character, @@ -496,7 +474,7 @@ public class ProxyService( launchExecutable(port, result.outputPath, os, character) } - private fun launchJar( + private fun launchJavaProcess( port: Int, operatingSystem: OperatingSystem, character: JagexCharacter?, @@ -511,7 +489,7 @@ public class ProxyService( try { val javConfigEndpoint = properties.getProperty(JAV_CONFIG_ENDPOINT) val launcher = RuneliteLauncher() - val args = listOf(getJava()) + launcher.getLaunchArgs( + val args = launcher.getLaunchArgs( port, rsa.publicKey.modulus.toString(16), javConfig = "http://127.0.0.1:$HTTP_SERVER_PORT/$javConfigEndpoint", diff --git a/proxy/src/main/kotlin/net/rsprox/proxy/runelite/RuneliteLauncher.kt b/proxy/src/main/kotlin/net/rsprox/proxy/runelite/RuneliteLauncher.kt index 1f7be231..fabb3a4c 100644 --- a/proxy/src/main/kotlin/net/rsprox/proxy/runelite/RuneliteLauncher.kt +++ b/proxy/src/main/kotlin/net/rsprox/proxy/runelite/RuneliteLauncher.kt @@ -7,6 +7,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import java.io.* import java.nio.file.Files +import java.nio.file.Paths import java.security.MessageDigest import java.security.Signature import java.security.cert.Certificate @@ -37,6 +38,26 @@ public class RuneliteLauncher { logger.info { "Initialising RuneLite launcher ${bootstrap.launcher.version}" } } + private fun getJava(): String { + val javaHome = Paths.get(System.getProperty("java.home")) + + if (!Files.exists(javaHome)) { + throw FileNotFoundException("JAVA_HOME is not set correctly! directory \"$javaHome\" does not exist.") + } + + var javaPath = Paths.get(javaHome.toString(), "bin", "java.exe") + + if (!Files.exists(javaPath)) { + javaPath = Paths.get(javaHome.toString(), "bin", "java") + } + + if (!Files.exists(javaPath)) { + throw FileNotFoundException("java executable not found in directory \"" + javaPath.parent + "\"") + } + + return javaPath.toAbsolutePath().toString() + } + public fun getLaunchArgs( port: Int, rsa: String, @@ -56,6 +77,7 @@ public class RuneliteLauncher { } return listOf( + getJava(), "-cp", classpath.toString(), bootstrap.launcher.mainClass,