diff --git a/core/src/com/ray3k/skincomposer/desktop/DesktopLauncher.java b/core/src/com/ray3k/skincomposer/desktop/DesktopLauncher.java index 9c8b3270..c0756f59 100644 --- a/core/src/com/ray3k/skincomposer/desktop/DesktopLauncher.java +++ b/core/src/com/ray3k/skincomposer/desktop/DesktopLauncher.java @@ -457,7 +457,7 @@ public void created(Lwjgl3Window lw) { } public static void main(String[] args) { - if (restartStartOnFirstThread()) { + if (StartupHelper.startNewJvmIfRequired()) { return; } @@ -503,73 +503,4 @@ public static void main(String[] args) { } } } - - public static boolean restartStartOnFirstThread() { - - String osName = System.getProperty("os.name").toLowerCase(); - if (!osName.startsWith("mac") && !osName.startsWith("darwin")) { - if (osName.contains("windows")) { -// Here, we are trying to work around an issue with how LWJGL3 loads its extracted .dll files. -// By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", which is usually in the user's home. -// If the user's name has non-ASCII (or some non-alphanumeric) characters in it, that would fail. -// By extracting to the relevant "ProgramData" folder, which is usually "C:\ProgramData", we typically avoid this. -// Some locales for Windows may translate "C:\ProgramData" to something with non-ASCII chars; we're out of luck then. - System.setProperty("java.io.tmpdir", System.getenv("ProgramData") + "/libGDX-temp"); - } - return false; - } - - // get current jvm process pid - String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; - // get environment variable on whether XstartOnFirstThread is enabled - String env = System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid); - - // if environment variable is "1" then XstartOnFirstThread is enabled - if (env != null && env.equals("1")) { - return false; - } - - // restart jvm with -XstartOnFirstThread - String separator = System.getProperty("file.separator"); - String classpath = System.getProperty("java.class.path"); - String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid); - String jvmPath = System.getProperty("java.home") + separator + "bin" + separator + "java"; - - List inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); - - ArrayList jvmArgs = new ArrayList<>(); - - jvmArgs.add(jvmPath); - jvmArgs.add("-XstartOnFirstThread"); - jvmArgs.addAll(inputArguments); - jvmArgs.add("-cp"); - jvmArgs.add(classpath); - jvmArgs.add(mainClass); - - // if you don't need console output, just enable these two lines - // and delete bits after it. This JVM will then terminate. - //ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs); - //processBuilder.start(); - - try { - ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs); - processBuilder.redirectErrorStream(true); - Process process = processBuilder.start(); - - InputStream is = process.getInputStream(); - InputStreamReader isr = new InputStreamReader(is); - BufferedReader br = new BufferedReader(isr); - String line; - - while ((line = br.readLine()) != null) { - System.out.println(line); - } - - process.waitFor(); - } catch (Exception e) { - e.printStackTrace(); - } - - return true; - } } diff --git a/core/src/com/ray3k/skincomposer/desktop/StartupHelper.java b/core/src/com/ray3k/skincomposer/desktop/StartupHelper.java new file mode 100644 index 00000000..b68a9284 --- /dev/null +++ b/core/src/com/ray3k/skincomposer/desktop/StartupHelper.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020 damios + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//Note, the above license and copyright applies to this file only. + +package com.ray3k.skincomposer.desktop; + +import org.lwjgl.system.macosx.LibC; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; + +/** + * Adds some utilities to ensure that the JVM was started with the + * {@code -XstartOnFirstThread} argument, which is required on macOS for LWJGL 3 + * to function. Also helps on Windows when users have names with characters from + * outside the Latin alphabet, a common cause of startup crashes. + *
+ * Based on this java-gaming.org post by kappa + * @author damios + */ +public class StartupHelper { + + private static final String JVM_RESTARTED_ARG = "jvmIsRestarted"; + + private StartupHelper() { + throw new UnsupportedOperationException(); + } + + /** + * Starts a new JVM if the application was started on macOS without the + * {@code -XstartOnFirstThread} argument. This also includes some code for + * Windows, for the case where the user's home directory includes certain + * non-Latin-alphabet characters (without this code, most LWJGL3 apps fail + * immediately for those users). Returns whether a new JVM was started and + * thus no code should be executed. + *

+ * Usage: + * + *


+     * public static void main(String... args) {
+     * 	if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
+     * 	// after this is the actual main method code
+     * }
+     * 
+ * + * @param redirectOutput + * whether the output of the new JVM should be rerouted to the + * old JVM, so it can be accessed in the same place; keeps the + * old JVM running if enabled + * @return whether a new JVM was started and thus no code should be executed + * in this one + */ + public static boolean startNewJvmIfRequired(boolean redirectOutput) { + String osName = System.getProperty("os.name").toLowerCase(); + if (!osName.contains("mac")) { + if (osName.contains("windows")) { +// Here, we are trying to work around an issue with how LWJGL3 loads its extracted .dll files. +// By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", which is usually the user's home. +// If the user's name has non-ASCII (or some non-alphanumeric) characters in it, that would fail. +// By extracting to the relevant "ProgramData" folder, which is usually "C:\ProgramData", we avoid this. + System.setProperty("java.io.tmpdir", System.getenv("ProgramData") + "/libGDX-temp"); + } + return false; + } + + // There is no need for -XstartOnFirstThread on Graal native image + if (!System.getProperty("org.graalvm.nativeimage.imagecode", "").isEmpty()) { + return false; + } + + long pid = LibC.getpid(); + + // check whether -XstartOnFirstThread is enabled + if ("1".equals(System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid))) { + return false; + } + + // check whether the JVM was previously restarted + // avoids looping, but most certainly leads to a crash + if ("true".equals(System.getProperty(JVM_RESTARTED_ARG))) { + System.err.println( + "There was a problem evaluating whether the JVM was started with the -XstartOnFirstThread argument."); + return false; + } + + // Restart the JVM with -XstartOnFirstThread + ArrayList jvmArgs = new ArrayList<>(); + String separator = System.getProperty("file.separator"); + // The following line is used assuming you target Java 8, the minimum for LWJGL3. + String javaExecPath = System.getProperty("java.home") + separator + "bin" + separator + "java"; + // If targeting Java 9 or higher, you could use the following instead of the above line: + //String javaExecPath = ProcessHandle.current().info().command().orElseThrow(); + + if (!(new File(javaExecPath)).exists()) { + System.err.println( + "A Java installation could not be found. If you are distributing this app with a bundled JRE, be sure to set the -XstartOnFirstThread argument manually!"); + return false; + } + + jvmArgs.add(javaExecPath); + jvmArgs.add("-XstartOnFirstThread"); + jvmArgs.add("-D" + JVM_RESTARTED_ARG + "=true"); + jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + jvmArgs.add("-cp"); + jvmArgs.add(System.getProperty("java.class.path")); + String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid); + if (mainClass == null) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + if (trace.length > 0) { + mainClass = trace[trace.length - 1].getClassName(); + } else { + System.err.println("The main class could not be determined."); + return false; + } + } + jvmArgs.add(mainClass); + + try { + if (!redirectOutput) { + ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs); + processBuilder.start(); + } else { + Process process = (new ProcessBuilder(jvmArgs)) + .redirectErrorStream(true).start(); + BufferedReader processOutput = new BufferedReader( + new InputStreamReader(process.getInputStream())); + String line; + + while ((line = processOutput.readLine()) != null) { + System.out.println(line); + } + + process.waitFor(); + } + } catch (Exception e) { + System.err.println("There was a problem restarting the JVM"); + e.printStackTrace(); + } + + return true; + } + + /** + * Starts a new JVM if the application was started on macOS without the + * {@code -XstartOnFirstThread} argument. Returns whether a new JVM was + * started and thus no code should be executed. Redirects the output of the + * new JVM to the old one. + *

+ * Usage: + * + *

+     * public static void main(String... args) {
+     * 	if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
+     * 	// the actual main method code
+     * }
+     * 
+ * + * @return whether a new JVM was started and thus no code should be executed + * in this one + */ + public static boolean startNewJvmIfRequired() { + return startNewJvmIfRequired(true); + } +} \ No newline at end of file