Skip to content

Commit

Permalink
Retry option for LaunchCommand
Browse files Browse the repository at this point in the history
  • Loading branch information
3arthqu4ke committed Aug 24, 2024
1 parent 8f411b6 commit 38fe5cf
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-matrix-xvfb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ jobs:
java: ${{ matrix.version.java }}
mc-runtime-test: ${{ matrix.version.type }}
xvfb: true
headlessmc-command: --jvm -Djava.awt.headless=true
headlessmc-command: --retries 2 --jvm -Djava.awt.headless=true
download-hmc: false
2 changes: 1 addition & 1 deletion .github/workflows/run-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ jobs:
java: ${{ matrix.version.java }}
mc-runtime-test: ${{ matrix.version.type }}
xvfb: false
headlessmc-command: -lwjgl --jvm -Djava.awt.headless=true
headlessmc-command: -lwjgl --retries 2 --jvm -Djava.awt.headless=true
download-hmc: false
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package me.earth.headlessmc.api.command;

import lombok.experimental.UtilityClass;
import me.earth.headlessmc.api.config.HasConfig;
import me.earth.headlessmc.api.config.Property;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
Expand Down Expand Up @@ -97,6 +99,10 @@ public static boolean hasFlag(String arg, String... args) {
return Arrays.stream(args).anyMatch(s -> s.equalsIgnoreCase(arg));
}

public static boolean flag(HasConfig ctx, String flg, Property<Boolean> invertFlag, Property<Boolean> alwaysFlag, String... args) {
return ctx.getConfig().get(alwaysFlag, false) || CommandUtil.hasFlag(flg, args) ^ ctx.getConfig().get(invertFlag, false);
}

public static @Nullable String getOption(String option, String... args) {
for (int i = 0; i < args.length; i++) {
if (option.equalsIgnoreCase(args[i]) && i < args.length - 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public interface LauncherProperties extends HmcProperties {

Property<Boolean> ALWAYS_LWJGL_FLAG = bool("hmc.always.lwjgl.flag");
Property<Boolean> ALWAYS_PAULS_FLAG = bool("hmc.always.pauls.flag");
Property<Boolean> ALWAYS_QUIT_FLAG = bool("hmc.always.quit.flag");
Property<Boolean> ALWAYS_JNDI_FLAG = bool("hmc.always.jndi.flag");
Property<Boolean> ALWAYS_LOOKUP_FLAG = bool("hmc.always.lookup.flag");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import lombok.val;
import me.earth.headlessmc.api.command.CommandException;
import me.earth.headlessmc.api.command.CommandUtil;
import me.earth.headlessmc.api.config.Property;
import me.earth.headlessmc.api.command.ParseUtil;
import me.earth.headlessmc.launcher.Launcher;
import me.earth.headlessmc.launcher.LauncherProperties;
import me.earth.headlessmc.launcher.auth.AuthException;
import me.earth.headlessmc.launcher.auth.LaunchAccount;
import me.earth.headlessmc.launcher.auth.ValidatedAccount;
import me.earth.headlessmc.launcher.files.FileManager;
import me.earth.headlessmc.launcher.launch.LaunchException;
import me.earth.headlessmc.launcher.launch.LaunchOptions;
import me.earth.headlessmc.launcher.version.Version;
Expand All @@ -18,30 +19,25 @@
import java.util.UUID;
import java.util.logging.Level;

import static me.earth.headlessmc.api.command.CommandUtil.flag;
import static me.earth.headlessmc.launcher.LauncherProperties.RE_THROW_LAUNCH_EXCEPTIONS;

@CustomLog
public class LaunchCommand extends AbstractVersionCommand {
public LaunchCommand(Launcher launcher) {
super(launcher, "launch", "Launches the game.");
args.put("<version/id>", "Name or id of the version to launch." +
" If you use the id you need to use the -id flag as well.");
args.put("-id",
"Use if you specified an id instead of a version name.");
args.put("-commands",
"Starts the game with the built-in command line support.");
args.put("-lwjgl", "Removes lwjgl code, causing Minecraft" +
" not to render anything.");
args.put("<version/id>", "Name or id of the version to launch. If you use the id you need to use the -id flag as well.");
args.put("-id", "Use if you specified an id instead of a version name.");
args.put("-commands", "Starts the game with the built-in command line support.");
args.put("-lwjgl", "Removes lwjgl code, causing Minecraft not to render anything.");
args.put("-inmemory", "Launches the game in the same JVM headlessmc is running in.");
args.put("-jndi", "Patches the Log4J vulnerability.");
args.put("-lookup", "Patches the Log4J vulnerability even harder.");
args.put("-paulscode", "Removes some error messages from the" +
" PaulsCode library which may annoy you if you started the" +
" game with the -lwjgl flag.");
// TODO: is this really necessary?
args.put("-noout", "Doesn't print Minecrafts output to the console.");
args.put("-paulscode", "Removes some error messages from the PaulsCode library which may annoy you if you started the game with the -lwjgl flag.");
args.put("-noout", "Doesn't print Minecrafts output to the console."); // TODO: is this really necessary?
args.put("-quit", "Quit HeadlessMc after launching the game.");
args.put("--jvm", "Jvm args to use.");
args.put("--retries", "The amount of times you want to retry running Minecraft.");
}

@Override
Expand All @@ -52,83 +48,38 @@ public void execute(Version version, String... args) throws CommandException {
ctx.getLoggingService().setLevel(Level.INFO, true);
val files = ctx.getFileManager().createRelative(uuid.toString());

boolean quit = flag("-quit", LauncherProperties.INVERT_QUIT_FLAG, args);
boolean quit = flag(ctx, "-quit", LauncherProperties.INVERT_QUIT_FLAG, LauncherProperties.ALWAYS_QUIT_FLAG, args);
int status = 0;
ClassLoader contextClassloader = Thread.currentThread().getContextClassLoader();
try {
if (!prepare) {
ctx.getCommandLine().close();
}

val process = ctx.getProcessFactory().run(
LaunchOptions.builder()
.account(getAccount())
.version(version)
.launcher(ctx)
.files(files)
.parseFlags(ctx, quit, args)
.prepare(prepare)
.build());
if (prepare) {
return;
}

if (process == null) {
ctx.log("InMemory main thread ended.");
Thread.currentThread().setContextClassLoader(contextClassloader);
}

if (quit || process == null) {
ctx.getExitManager().exit(0);
return;
}

try {
status = process.waitFor();
ctx.log("Minecraft exited with code: " + status);
if (status != 0 && ctx.getConfig().get(RE_THROW_LAUNCH_EXCEPTIONS, false)) {
throw new IllegalStateException("Minecraft exited with code " + status);
}
} catch (InterruptedException ie) {
ctx.log("Launcher has been interrupted...");
Thread.currentThread().interrupt();
}
status = runProcess(version, files, quit, prepare, args);
} catch (IOException | LaunchException | AuthException e) {
status = -1;
log.error(e);
ctx.log(String.format(
"Couldn't launch %s: %s", version.getName(), e.getMessage()));
ctx.log(String.format("Couldn't launch %s: %s", version.getName(), e.getMessage()));
if (ctx.getConfig().get(RE_THROW_LAUNCH_EXCEPTIONS, false)) {
throw new IllegalStateException(e);
}
} catch (Throwable t) {
status = -1;
val msg = String.format(
"Couldn't launch %s: %s", version.getName(), t.getMessage());
val msg = String.format("Couldn't launch %s: %s", version.getName(), t.getMessage());
log.error(msg, t);
ctx.log(msg);
throw t;
} finally {
Thread.currentThread().setContextClassLoader(contextClassloader);
// for some reason both ShutdownHooks and File.deleteOnExit are
// not really working, that's why we Main.deleteOldFiles, too.
if (!CommandUtil.hasFlag("-keep", args)
&& !ctx.getConfig().get(LauncherProperties.KEEP_FILES, false)) {
try {
log.info("Deleting " + files.getBase().getName());
ctx.getFileManager().delete(files.getBase());
} catch (IOException e) {
log.error("Couldn't delete files of game "
+ files.getBase().getName()
+ ": " + e.getMessage());
}
}

cleanup(files, args);
if (!prepare && !CommandUtil.hasFlag("-stay", args)) {
ctx.getExitManager().exit(status);
}
}

if (status != 0 && ctx.getConfig().get(RE_THROW_LAUNCH_EXCEPTIONS, false)) {
throw new IllegalStateException("Minecraft exited with code " + status);
}

if (!prepare) {
try {
ctx.getCommandLine().open(ctx);
Expand All @@ -138,8 +89,92 @@ public void execute(Version version, String... args) throws CommandException {
}
}

private boolean flag(String flg, Property<Boolean> inv, String... args) {
return CommandUtil.hasFlag(flg, args) ^ ctx.getConfig().get(inv, false);
private int runProcess(Version version, FileManager files, boolean quit, boolean prepare, String... args)
throws CommandException, LaunchException, AuthException {
int status = 0;
LaunchAccount account = getAccount();
String retriesOption = CommandUtil.getOption("--retries", args);
int retries = 0;
if (retriesOption != null) {
retries = ParseUtil.parseI(retriesOption);
}

Throwable throwable = null;
for (int i = 0; i < retries + 1; i++) {
if (i > 0) {
log.warn("Retrying to launch Minecraft: " + i);
}

try {
Process process = ctx.getProcessFactory().run(
LaunchOptions.builder()
.account(account)
.version(version)
.launcher(ctx)
.files(files)
.parseFlags(ctx, quit, args)
.prepare(prepare)
.build()
);

if (prepare) {
return 0;
}

if (process == null) {
ctx.log("InMemory main thread ended.");
}

if (quit || process == null) {
cleanup(files, args);
ctx.getExitManager().exit(0);
return 0;
}

try {
status = process.waitFor();
ctx.log("Minecraft exited with code: " + status);
} catch (InterruptedException ie) {
ctx.log("Launcher has been interrupted...");
Thread.currentThread().interrupt();
}

if (status == 0) {
break;
}
} catch (Throwable t) {
status = -1;
log.error("Failed to start Minecraft on try " + i, t);
if (throwable == null) {
throwable = t;
} else {
throwable.addSuppressed(t);
}
}
}

if (status != 0 && throwable != null) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}

throw new LaunchException(throwable);
}

return status;
}

private void cleanup(FileManager files, String... args) {
// for some reason both ShutdownHooks and File.deleteOnExit are
// not really working, that's why we Main.deleteOldFiles, too.
if (!CommandUtil.hasFlag("-keep", args) && !ctx.getConfig().get(LauncherProperties.KEEP_FILES, false)) {
try {
log.info("Deleting " + files.getBase().getName());
ctx.getFileManager().delete(files.getBase());
} catch (IOException e) {
log.error("Couldn't delete files of game " + files.getBase().getName(), e);
}
}
}

protected LaunchAccount getAccount() throws CommandException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public void launch() throws IOException, LaunchException, AuthException {
}

protected void java9Launch(URL[] classpathUrls, String mainClass, List<String> gameArgs) {
ClassLoader contextClassloader = Thread.currentThread().getContextClassLoader();
try {
Class<?> bootstrapLauncherClass = Class.forName("me.earth.headlessmc.modlauncher.LayeredBootstrapLauncher");
Constructor<?> constructor = bootstrapLauncherClass.getConstructor(List.class, URL[].class, String.class);
Expand All @@ -94,6 +95,8 @@ protected void java9Launch(URL[] classpathUrls, String mainClass, List<String> g
launch.invoke(bootstrapLauncher, (Object) gameArgs.toArray(new String[0]));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
} finally {
Thread.currentThread().setContextClassLoader(contextClassloader);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import lombok.CustomLog;
import lombok.Data;
import me.earth.headlessmc.api.command.CommandUtil;
import me.earth.headlessmc.api.config.HasConfig;
import me.earth.headlessmc.api.config.Property;
import me.earth.headlessmc.launcher.Launcher;
import me.earth.headlessmc.launcher.LauncherProperties;
import me.earth.headlessmc.launcher.auth.LaunchAccount;
import me.earth.headlessmc.launcher.files.FileManager;
import me.earth.headlessmc.launcher.version.Version;
Expand All @@ -17,6 +14,7 @@
import java.util.Collections;
import java.util.List;

import static me.earth.headlessmc.api.command.CommandUtil.flag;
import static me.earth.headlessmc.launcher.LauncherProperties.*;

@Data
Expand Down Expand Up @@ -78,10 +76,6 @@ public LaunchOptionsBuilder parseJvmArgs(String... args) {

return this;
}

private boolean flag(HasConfig ctx, String flg, Property<Boolean> invertFlag, Property<Boolean> alwaysFlag, String... args) {
return ctx.getConfig().get(alwaysFlag, false) || CommandUtil.hasFlag(flg, args) ^ ctx.getConfig().get(invertFlag, false);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import me.earth.headlessmc.api.config.ConfigImpl;
import me.earth.headlessmc.api.config.HasConfig;
import me.earth.headlessmc.api.exit.ExitManager;
import me.earth.headlessmc.launcher.auth.AccountManager;
import me.earth.headlessmc.launcher.auth.AccountStore;
import me.earth.headlessmc.launcher.auth.AccountValidator;
import me.earth.headlessmc.launcher.auth.ValidatedAccount;
import me.earth.headlessmc.launcher.auth.*;
import me.earth.headlessmc.launcher.download.ChecksumService;
import me.earth.headlessmc.launcher.download.DownloadService;
import me.earth.headlessmc.launcher.download.MockDownloadService;
Expand All @@ -33,6 +30,10 @@ public class LauncherMock {
public static final Launcher INSTANCE;

static {
INSTANCE = create();
}

public static Launcher create() {
val base = FileManager.forPath("build");
val fileManager = base.createRelative("fileManager");
val configs = new ConfigService(fileManager);
Expand All @@ -50,12 +51,14 @@ public class LauncherMock {

DownloadService downloadService = new MockDownloadService();
val versionSpecificModManager = new VersionSpecificModManager(downloadService, fileManager.createRelative("specifics"));
INSTANCE = new Launcher(hmc, versions, mcFiles, mcFiles,
Launcher launcher = new Launcher(hmc, versions, mcFiles, mcFiles,
new ChecksumService(), new MockDownloadService(),
fileManager, new MockProcessFactory(downloadService, mcFiles, configs, os), configs,
javas, accounts, versionSpecificModManager, new PluginManager());

INSTANCE.getConfigService().setConfig(ConfigImpl.empty());
launcher.getConfigService().setConfig(ConfigImpl.empty());

return launcher;
}

private static final class DummyAccountManager extends AccountManager {
Expand Down
Loading

0 comments on commit 38fe5cf

Please sign in to comment.