Skip to content

Commit

Permalink
Add a shutdown hook which dumps diagnostic data when Recaf closed wit…
Browse files Browse the repository at this point in the history
…h a non-standard exit code
  • Loading branch information
Col-E committed Oct 19, 2024
1 parent 18cb967 commit f975adb
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 28 deletions.
4 changes: 2 additions & 2 deletions recaf-core/src/main/java/software/coley/recaf/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static Recaf get() {
- Build rev: {}
- Build date: {}
- Build hash: {}""";
logger.info(fmt, VERSION, GIT_REVISION, BUILD_DATE, GIT_SHA);
logger.info(fmt, VERSION, GIT_REVISION, GIT_DATE, GIT_SHA);
long then = System.currentTimeMillis();

// Create the Recaf container
Expand All @@ -45,7 +45,7 @@ public static Recaf get() {
logger.info("Recaf CDI container created in {}ms", System.currentTimeMillis() - then);
} catch (Throwable t) {
logger.error("Failed to create Recaf CDI container", t);
System.exit(ExitCodes.ERR_CDI_INIT_FAILURE);
ExitDebugLoggingHook.exit(ExitCodes.ERR_CDI_INIT_FAILURE);
}
}
return instance;
Expand Down
16 changes: 8 additions & 8 deletions recaf-core/src/main/java/software/coley/recaf/ExitCodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
*/
public class ExitCodes {
public static final int SUCCESS = 0;
public static final int ERR_UNKNOWN = 100;
public static final int ERR_CLASS_NOT_FOUND = 101;
public static final int ERR_NO_SUCH_METHOD = 102;
public static final int ERR_INVOKE_TARGET = 103;
public static final int ERR_ACCESS_TARGET = 104;
public static final int ERR_OLD_JFX_VERSION = 105;
public static final int ERR_UNKNOWN_JFX_VERSION = 106;
public static final int ERR_CDI_INIT_FAILURE = 107;
public static final int ERR_FX_UNKNOWN = 100;
public static final int ERR_FX_CLASS_NOT_FOUND = 101;
public static final int ERR_FX_NO_SUCH_METHOD = 102;
public static final int ERR_FX_INVOKE_TARGET = 103;
public static final int ERR_FX_ACCESS_TARGET = 104;
public static final int ERR_FX_OLD_VERSION = 105;
public static final int ERR_FX_UNKNOWN_VERSION = 106;
public static final int INTELLIJ_TERMINATION = 130;
public static final int ERR_CDI_INIT_FAILURE = 150;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package software.coley.recaf;

import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import jakarta.annotation.Nonnull;
import org.slf4j.Logger;
import software.coley.recaf.analytics.logging.Logging;
import software.coley.recaf.services.file.RecafDirectoriesConfig;
import software.coley.recaf.util.JavaVersion;
import software.coley.recaf.util.LookupUtil;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A hook registered to {@code java.lang.Shutdown} and not {@link Runtime#addShutdownHook(Thread)}.
* This allows us to use the {@link StackWalker} API to detect the exit code from {@link System#exit(int)}
*
* @author Matt Coley
*/
public class ExitDebugLoggingHook {
private static final int UNKNOWN_CODE = -1337420;
private static final Logger logger = Logging.get(ExitDebugLoggingHook.class);
private static int exitCode = UNKNOWN_CODE;
private static boolean printConfigs;
private static Thread mainThread;
private static MethodHandles.Lookup lookup;

/**
* Register the shutdown hook.
*/
public static void register() {
lookup = LookupUtil.lookup();
try {
// We use this instead of the Runtime shutdown hook thread because this will run on the same thread
// as the call to System.exit(int)
Class<?> shutdown = lookup.findClass("java.lang.Shutdown");
MethodHandle add = lookup.findStatic(shutdown, "add", MethodType.methodType(void.class, int.class, boolean.class, Runnable.class));
add.invoke(9, false, (Runnable) ExitDebugLoggingHook::run);
} catch (Throwable t) {
logger.error("Failed to add exit-hooking debug dumping shutdown hook", t);

// Use fallback shutdown hook which checks for manual exit codes being set.
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (exitCode != UNKNOWN_CODE)
handle(exitCode);
}));
}
}

private static void run() {
// If we've set the exit code manually, use that.
if (exitCode != UNKNOWN_CODE) {
handle(exitCode);
return;
}

// We didn't do the exit, so lets try and see if we can figure out who did it.
try {
AtomicBoolean visited = new AtomicBoolean();
Class<?> shutdown = lookup.findClass("java.lang.Shutdown");
Class<?> stackFrameClass = Class.forName("java.lang.LiveStackFrame");
MethodHandle getLocals = lookup.findVirtual(stackFrameClass, "getLocals", MethodType.methodType(Object[].class))
.asType(MethodType.methodType(Object[].class, Object.class));
Method getStackWalker = stackFrameClass.getDeclaredMethod("getStackWalker", Set.class);
getStackWalker.setAccessible(true);
StackWalker stackWalker = (StackWalker) getStackWalker.invoke(null, Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE, StackWalker.Option.SHOW_HIDDEN_FRAMES));
stackWalker.forEach(frame -> {
try {
if (!visited.get() && frame.getDeclaringClass() == shutdown && frame.getMethodName().equals("exit")) {
Object[] locals = (Object[]) getLocals.invoke(frame);
handle(Integer.parseInt(String.valueOf(locals[0])));
visited.set(true);
}
} catch (Throwable t) {
throw new IllegalStateException(t);
}
});
} catch (Throwable t) {
// If this cursed abomination breaks, we want to know about it
logger.error("""
Failed to detect application exit code.
Please report that the exit debugger has failed.
https://github.com/Col-E/Recaf/issues/new?labels=bug&title=Error%20Debugger%20Hook%20fails%20on%20Java%20{V}
""".replace("{V}", String.valueOf(JavaVersion.get())), t);
}
}

private static void handle(int code) {
// Skip on successful closure
if (code == ExitCodes.SUCCESS || code == ExitCodes.INTELLIJ_TERMINATION)
return;

if (code == UNKNOWN_CODE)
System.out.println("Exit code: <error>");
else
System.out.println("Exit code: " + code);

System.out.println("Java");
System.out.println(" - Version (Runtime): " + System.getProperty("java.runtime.version", "<unknown>"));
System.out.println(" - Version (Raw): " + JavaVersion.get());
System.out.println(" - Vendor: " + System.getProperty("java.vm.vendor", "<unknown>"));
System.out.println(" - Home: " + System.getProperty("java.home", "<unknown>"));
System.out.println("JavaFX");
System.out.println(" - Version (Runtime): " + System.getProperty("javafx.runtime.version", "<uninitialized>"));
System.out.println(" - Version (Raw): " + System.getProperty("javafx.version", "<uninitialized>"));
System.out.println("Operating System");
System.out.println(" - Name: " + System.getProperty("os.name"));
System.out.println(" - Version: " + System.getProperty("os.version"));
System.out.println(" - Architecture: " + System.getProperty("os.arch"));
System.out.println(" - Processors: " + Runtime.getRuntime().availableProcessors());
System.out.println(" - Path Separator: " + File.pathSeparator);
System.out.println("Recaf" + JavaVersion.get());
System.out.println(" - Version: " + RecafBuildConfig.VERSION);
System.out.println(" - Build hash: " + RecafBuildConfig.GIT_SHA);
System.out.println(" - Build date: " + RecafBuildConfig.GIT_DATE);

String command = System.getProperty("sun.java.command", "");
if (command != null) {
System.out.println("Launch");
System.out.println(" - Args: " + command);
}

String[] classPath = System.getProperty("java.class.path").split(File.pathSeparator);
System.out.println("Classpath:");
for (String pathEntry : classPath) {
File file = new File(pathEntry);
if (file.isFile()) {
System.out.println(" - File: " + pathEntry);
try {
System.out.println(" - SHA1: " + createSha1(file));
} catch (Exception ex) {
System.out.println(" - SHA1: <error>");
}
} else if (file.isDirectory()) {
System.out.println(" - Directory: " + pathEntry);
}
}

Path root = RecafDirectoriesConfig.createBaseDirectory().resolve("config");
if (printConfigs && Files.isDirectory(root)) {
System.out.println("Configs");
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(path -> path.getFileName().toString().endsWith("-config.json")).forEach(path -> {
String configName = path.getFileName().toString();

// Skip certain configs
if (configName.contains("service.ui.bind-"))
return;
if (configName.contains("service.ui.snippets-config"))
return;
if (configName.contains("service.io.recent-workspaces-config"))
return;
if (configName.contains("service.decompile.impl.decompiler-"))
return;

System.out.println(" - " + configName + ":");
try {
String indent = " ";
String configJson = Files.readString(path);
String indented = indent + Arrays.stream(configJson.split("\n"))
.collect(Collectors.joining("\n" + indent));
System.out.println(indented);
} catch (Throwable t) {
System.out.println(" - <error>");
}
});
} catch (Exception ex) {
System.out.println(" - <error>");
}
}

System.out.println("Threads");
Thread.getAllStackTraces().forEach((thread, trace) -> {
System.out.println(" - " + thread.getName() + " [" + thread.getState().name() + "]");
for (StackTraceElement element : trace) {
System.out.println(" - " + element);
}
});
}

@Nonnull
@SuppressWarnings("all")
private static String createSha1(@Nonnull File file) throws Exception {
long length = file.length();
if (length > Integer.MAX_VALUE)
throw new IOException("File too large to hash");
Hasher hasher = Hashing.sha1().newHasher((int) length);
try (InputStream fis = new FileInputStream(file)) {
int n = 0;
byte[] buffer = new byte[8192];
while (n != -1) {
n = fis.read(buffer);
if (n > 0)
hasher.putBytes(buffer, 0, n);
}
return hasher.hash().toString();
}
}

public static void exit(int exitCode) {
ExitDebugLoggingHook.exitCode = exitCode;
System.exit(exitCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,11 @@ private Path resolveDirectory(@Nonnull String dir) {
return path;
}

/**
* @return Root directory for storing Recaf data.
*/
@Nonnull
private static Path createBaseDirectory() {
public static Path createBaseDirectory() {
// Try system property first
String recafDir = System.getProperty("RECAF_DIR");
if (recafDir == null) // Next try looking for an environment variable
Expand All @@ -160,9 +163,8 @@ private static Path createBaseDirectory() {

// The directories library can break on some version of windows, but it will always
// resolve to '%APPDATA%' at the end of the day. So we'll just do that ourselves here,
if (PlatformType.get() == PlatformType.WINDOWS) {
if (PlatformType.get() == PlatformType.WINDOWS)
return Paths.get(System.getenv("APPDATA"), "Recaf");
}

// Use generic data/config location
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package software.coley.recaf.util;

import jakarta.annotation.Nonnull;
import software.coley.recaf.analytics.SystemInformation;

import java.awt.*;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
Expand Down Expand Up @@ -35,11 +37,11 @@ public static Dimension getScreenSize() {
* to be launched.
*/
public static void showDocument(@Nonnull URI uri) throws IOException {
final Runtime rt = Runtime.getRuntime();
switch (PlatformType.get()) {
case MAC -> Runtime.getRuntime().exec("open " + uri);
case WINDOWS -> Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + uri);
case MAC -> rt.exec(new String[]{"open", uri.toString()});
case WINDOWS -> rt.exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", uri.toString()});
case LINUX -> {
Runtime rt = Runtime.getRuntime();
String[] browsers = new String[]{"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla"};
for (String browser : browsers) {
try (InputStream in = rt.exec(new String[]{"which", browser}).getInputStream()) {
Expand All @@ -51,7 +53,7 @@ public static void showDocument(@Nonnull URI uri) throws IOException {
}
throw new IOException("No browser found");
}
default -> throw new IllegalStateException("Unsupported OS");
default -> throw new IllegalStateException("Unsupported OS: " + SystemInformation.OS_NAME);
}
}

Expand Down
8 changes: 6 additions & 2 deletions recaf-ui/src/main/java/software/coley/recaf/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import software.coley.recaf.launch.LaunchArguments;
import software.coley.recaf.launch.LaunchCommand;
import software.coley.recaf.launch.LaunchHandler;
import software.coley.recaf.services.file.RecafDirectoriesConfig;
import software.coley.recaf.services.plugin.PluginContainer;
import software.coley.recaf.services.plugin.PluginException;
import software.coley.recaf.services.file.RecafDirectoriesConfig;
import software.coley.recaf.services.plugin.PluginManager;
import software.coley.recaf.services.plugin.discovery.DirectoryPluginDiscoverer;
import software.coley.recaf.services.script.ScriptEngine;
Expand Down Expand Up @@ -52,6 +52,10 @@ public class Main {
* Application arguments.
*/
public static void main(String[] args) {
// Add a shutdown hook which dumps system information to console.
// Should provide useful information that users can copy/paste to us for diagnosing problems.
ExitDebugLoggingHook.register();

// Add a class reference for our UI module.
Bootstrap.setWeldConsumer(weld -> weld.addPackage(true, Main.class));

Expand All @@ -71,7 +75,7 @@ public static void main(String[] args) {
if (!launchArgValues.isHeadless()) {
int validationCode = JFXValidation.validateJFX();
if (validationCode != 0) {
System.exit(validationCode);
ExitDebugLoggingHook.exit(validationCode);
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void start(Stage stage) {
stage.setScene(scene);
stage.getIcons().add(Icons.getImage(Icons.LOGO));
stage.setTitle("Recaf");
stage.setOnCloseRequest(e -> System.exit(0));
stage.setOnCloseRequest(e -> ExitDebugLoggingHook.exit(0));
stage.show();

// Register main window
Expand Down
Loading

0 comments on commit f975adb

Please sign in to comment.