From a2462c6691e355c7c06e6e833b74bb41d222989c Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Feb 2026 23:44:03 -0500 Subject: [PATCH] Add crash diagnostics dialog with process output capture When Wine/Box64 crashes, users currently see nothing - the app silently restarts. This change shows a dialog with the exit code, signal name (e.g., "Segmentation fault"), and the last 200 lines of process output so users can diagnose issues or share crash info in bug reports. Changes: - ProcessHelper: Always capture stdout/stderr to a 200-line ring buffer, not just when debug callbacks are registered. Added getCrashLogSnapshot() and clearCrashLog() methods. - XServerDisplayActivity: Check exit code in termination callback. If non-zero, show crash dialog instead of silently restarting. Graceful exits (status == 0) still restart normally. - Added signal name mapping for common crash signals (SIGSEGV, SIGABRT, SIGILL, SIGKILL, etc.) --- .../com/winlator/XServerDisplayActivity.java | 45 ++++++++++++++++++- .../java/com/winlator/core/ProcessHelper.java | 24 ++++++++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/winlator/XServerDisplayActivity.java b/app/src/main/java/com/winlator/XServerDisplayActivity.java index 1281d09e32..2b81e24908 100644 --- a/app/src/main/java/com/winlator/XServerDisplayActivity.java +++ b/app/src/main/java/com/winlator/XServerDisplayActivity.java @@ -346,6 +346,43 @@ private void exit() { AppUtils.restartApplication(this); } + private void showCrashDialog(int exitCode) { + String signalName = getSignalName(exitCode); + String exitInfo = "Exit code: " + exitCode + (signalName != null ? " (" + signalName + ")" : ""); + + ArrayList logLines = ProcessHelper.getCrashLogSnapshot(); + StringBuilder logText = new StringBuilder(); + logText.append(exitInfo).append("\n\n"); + if (logLines.isEmpty()) { + logText.append("No debug output captured.\nEnable Wine debug or Box86/64 logs in Settings for more detail."); + } else { + logText.append("Last ").append(logLines.size()).append(" lines of output:\n\n"); + for (String line : logLines) logText.append(line).append("\n"); + } + + ContentDialog dialog = new ContentDialog(this); + dialog.setTitle(getString(R.string.process_crashed)); + dialog.setCancelable(false); + dialog.setMessage(logText.toString()); + dialog.setOnConfirmCallback(this::exit); + dialog.show(); + } + + private static String getSignalName(int exitCode) { + if (exitCode < 128) return null; + switch (exitCode - 128) { + case 4: return "Illegal instruction"; + case 6: return "Aborted"; + case 7: return "Bus error"; + case 8: return "Floating point exception"; + case 9: return "Killed"; + case 11: return "Segmentation fault"; + case 13: return "Broken pipe"; + case 15: return "Terminated"; + default: return "Signal " + (exitCode - 128); + } + } + private void setupWineSystemFiles() { String appVersion = String.valueOf(AppUtils.getVersionCode(this)); String imgVersion = String.valueOf(imageFs.getVersion()); @@ -449,7 +486,13 @@ else if (audioDriver.equals("pulseaudio")) { } guestProgramLauncherComponent.setEnvVars(envVars); - guestProgramLauncherComponent.setTerminationCallback((status) -> exit()); + guestProgramLauncherComponent.setTerminationCallback((status) -> { + if (status != 0) { + runOnUiThread(() -> showCrashDialog(status)); + } else { + exit(); + } + }); environment.addComponent(guestProgramLauncherComponent); if (isGenerateWineprefix()) generateWineprefix(); diff --git a/app/src/main/java/com/winlator/core/ProcessHelper.java b/app/src/main/java/com/winlator/core/ProcessHelper.java index 31d237d2b2..dc6bc94f49 100644 --- a/app/src/main/java/com/winlator/core/ProcessHelper.java +++ b/app/src/main/java/com/winlator/core/ProcessHelper.java @@ -16,6 +16,8 @@ public abstract class ProcessHelper { private static final ArrayList> debugCallbacks = new ArrayList<>(); private static final byte SIGCONT = 18; private static final byte SIGSTOP = 19; + private static final int CRASH_LOG_BUFFER_SIZE = 200; + private static final ArrayList crashLogBuffer = new ArrayList<>(); public static void suspendProcess(int pid) { Process.sendSignal(pid, SIGSTOP); @@ -46,10 +48,8 @@ public static int exec(String command, String[] envp, File workingDir, Callback< pid = pidField.getInt(process); pidField.setAccessible(false); - if (!debugCallbacks.isEmpty()) { - createDebugThread(process.getInputStream()); - createDebugThread(process.getErrorStream()); - } + createDebugThread(process.getInputStream()); + createDebugThread(process.getErrorStream()); if (terminationCallback != null) createWaitForThread(process, terminationCallback); } @@ -63,6 +63,10 @@ private static void createDebugThread(final InputStream inputStream) { String line; while ((line = reader.readLine()) != null) { if (PRINT_DEBUG) System.out.println(line); + synchronized (crashLogBuffer) { + crashLogBuffer.add(line); + while (crashLogBuffer.size() > CRASH_LOG_BUFFER_SIZE) crashLogBuffer.remove(0); + } synchronized (debugCallbacks) { if (!debugCallbacks.isEmpty()) { for (Callback callback : debugCallbacks) callback.call(line); @@ -102,6 +106,18 @@ public static void removeDebugCallback(Callback callback) { } } + public static ArrayList getCrashLogSnapshot() { + synchronized (crashLogBuffer) { + return new ArrayList<>(crashLogBuffer); + } + } + + public static void clearCrashLog() { + synchronized (crashLogBuffer) { + crashLogBuffer.clear(); + } + } + public static String[] splitCommand(String command) { ArrayList result = new ArrayList<>(); boolean startedQuotes = false; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4879afecf5..7e66c37c89 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,4 +225,5 @@ Warning: Installing a new version of Wine is recommended for testing purposes only. Startup Selection Wine Debug Channel + Process Crashed \ No newline at end of file