Skip to content

Commit

Permalink
Use current StartupHelper from Liftoff. (#149)
Browse files Browse the repository at this point in the history
This should help Mac compatibility, as noted on the Discord.
  • Loading branch information
tommyettinger authored May 20, 2024
1 parent 88400cc commit 230ea61
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 70 deletions.
71 changes: 1 addition & 70 deletions core/src/com/ray3k/skincomposer/desktop/DesktopLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ public void created(Lwjgl3Window lw) {
}

public static void main(String[] args) {
if (restartStartOnFirstThread()) {
if (StartupHelper.startNewJvmIfRequired()) {
return;
}

Expand Down Expand Up @@ -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<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();

ArrayList<String> 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;
}
}
179 changes: 179 additions & 0 deletions core/src/com/ray3k/skincomposer/desktop/StartupHelper.java
Original file line number Diff line number Diff line change
@@ -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.
* <br>
* <a href="https://jvm-gaming.org/t/starting-jvm-on-mac-with-xstartonfirstthread-programmatically/57547">Based on this java-gaming.org post by kappa</a>
* @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.
* <p>
* <u>Usage:</u>
*
* <pre><code>
* 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
* }
* </code></pre>
*
* @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<String> 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.
* <p>
* <u>Usage:</u>
*
* <pre>
* public static void main(String... args) {
* if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
* // the actual main method code
* }
* </pre>
*
* @return whether a new JVM was started and thus no code should be executed
* in this one
*/
public static boolean startNewJvmIfRequired() {
return startNewJvmIfRequired(true);
}
}

0 comments on commit 230ea61

Please sign in to comment.