diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index 4d6f0442..ae19f5e6 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -6,6 +6,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.update.XPipeDistributionType; +import io.xpipe.app.util.PlatformState; import io.xpipe.core.process.OsType; import javafx.application.Application; import javafx.application.Platform; @@ -23,10 +24,6 @@ public class App extends Application { private Stage stage; private Image icon; - public static boolean isPlatformRunning() { - return APP != null; - } - public static App getApp() { return APP; } @@ -35,6 +32,7 @@ public class App extends Application { public void start(Stage primaryStage) { TrackEvent.info("Application launched"); APP = this; + PlatformState.setCurrent(PlatformState.RUNNING); stage = primaryStage; icon = AppImages.image("logo.png"); diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 49188282..7906fca7 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -3,10 +3,7 @@ package io.xpipe.app.core.mode; import io.xpipe.app.comp.storage.collection.SourceCollectionViewState; import io.xpipe.app.comp.storage.store.StoreViewState; import io.xpipe.app.core.*; -import io.xpipe.app.issue.ErrorAction; -import io.xpipe.app.issue.ErrorHandler; -import io.xpipe.app.issue.LogErrorHandler; -import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.issue.*; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.FileBridge; @@ -59,9 +56,9 @@ public class BaseMode extends OperationMode { @Override public ErrorHandler getErrorHandler() { var log = new LogErrorHandler(); - return event -> { + return new SyncErrorHandler(event -> { log.handle(event); ErrorAction.ignore().handle(event); - }; + }); } } diff --git a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java index 4cbe5a20..8b8ac093 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java @@ -4,6 +4,7 @@ import io.xpipe.app.core.App; import io.xpipe.app.core.AppGreetings; import io.xpipe.app.issue.*; import io.xpipe.app.update.UpdateChangelogAlert; +import io.xpipe.app.util.PlatformState; import javafx.application.Platform; import java.util.concurrent.CountDownLatch; @@ -17,7 +18,7 @@ public class GuiMode extends PlatformMode { @Override public void onSwitchTo() { - if (!App.isPlatformRunning()) { + if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) { super.platformSetup(); } @@ -44,7 +45,7 @@ public class GuiMode extends PlatformMode { @Override public void onSwitchFrom() { - if (App.isPlatformRunning()) { + if (PlatformState.getCurrent() == PlatformState.RUNNING) { TrackEvent.info("mode", "Closing window"); App.getApp().close(); waitForPlatform(); @@ -54,9 +55,9 @@ public class GuiMode extends PlatformMode { @Override public ErrorHandler getErrorHandler() { var log = new LogErrorHandler(); - return event -> { + return new SyncErrorHandler(event -> { log.handle(event); ErrorHandlerComp.showAndWait(event); - }; + }); } } diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java index 52043962..558925da 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java @@ -6,12 +6,11 @@ import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorHandler; -import io.xpipe.app.issue.SentryErrorHandler; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.launcher.LauncherCommand; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.app.util.XPipeSession; +import io.xpipe.core.util.XPipeDaemonMode; import org.apache.commons.lang3.function.FailableRunnable; import java.util.ArrayList; @@ -77,6 +76,11 @@ public abstract class OperationMode { OperationMode.shutdown(true, false); })); + // Handle uncaught exceptions + Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { + ErrorEvent.fromThrowable(ex).build().handle(); + }); + // if (true) { // throw new OutOfMemoryError(); // } @@ -84,7 +88,6 @@ public abstract class OperationMode { TrackEvent.info("mode", "Initial setup"); AppProperties.init(); XPipeSession.init(AppProperties.get().getBuildUuid()); - SentryErrorHandler.init(); AppChecks.checkDirectoryPermissions(); AppLogs.init(); AppProperties.logArguments(args); diff --git a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java index 968a3090..1b114707 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java @@ -6,6 +6,7 @@ import io.xpipe.app.core.*; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.update.UpdateAvailableAlert; +import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.ThreadHelper; import javafx.application.Application; import javafx.application.Platform; @@ -89,7 +90,7 @@ public abstract class PlatformMode extends OperationMode { protected void waitForPlatform() { // The platform thread waits for the shutdown hook to finish in case SIGTERM is sent. // Therefore, we do not wait for the platform when being in a shutdown hook. - if (App.isPlatformRunning() && !Platform.isFxApplicationThread() && !OperationMode.isInShutdownHook()) { + if (PlatformState.getCurrent() == PlatformState.RUNNING && !Platform.isFxApplicationThread() && !OperationMode.isInShutdownHook()) { TrackEvent.info("mode", "Waiting for platform thread ..."); CountDownLatch latch = new CountDownLatch(1); Platform.runLater(latch::countDown); @@ -116,6 +117,7 @@ public abstract class PlatformMode extends OperationMode { TrackEvent.info("mode", "Shutting down platform components"); onSwitchFrom(); Platform.exit(); + PlatformState.setCurrent(PlatformState.EXITED); TrackEvent.info("mode", "Platform shutdown finished"); BACKGROUND.finalTeardown(); } diff --git a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java index 54b8ddcb..b835deaf 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java @@ -1,9 +1,9 @@ package io.xpipe.app.core.mode; import com.dustinredmond.fxtrayicon.FXTrayIcon; -import io.xpipe.app.core.App; import io.xpipe.app.core.AppTray; import io.xpipe.app.issue.*; +import io.xpipe.app.util.PlatformState; public class TrayMode extends PlatformMode { @@ -19,7 +19,7 @@ public class TrayMode extends PlatformMode { @Override public void onSwitchTo() { - if (!App.isPlatformRunning()) { + if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) { super.platformSetup(); } @@ -45,13 +45,13 @@ public class TrayMode extends PlatformMode { @Override public ErrorHandler getErrorHandler() { var log = new LogErrorHandler(); - return event -> { + return new SyncErrorHandler(event -> { // Check if tray initialization is finished if (AppTray.get() != null) { AppTray.get().getErrorHandler().handle(event); } log.handle(event); ErrorAction.ignore().handle(event); - }; + }); } } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorAction.java b/app/src/main/java/io/xpipe/app/issue/ErrorAction.java index 1d33a5f4..c2daa839 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorAction.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorAction.java @@ -39,7 +39,7 @@ public interface ErrorAction { @Override public boolean handle(ErrorEvent event) { event.clearAttachments(); - SentryErrorHandler.report(event); + SentryErrorHandler.getInstance().handle(event); return true; } }; diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java index 95ff0e5a..7d518208 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -27,6 +27,12 @@ public class ErrorEvent { @Singular private List attachments; + private String userReport; + + public void attachUserReport(String text) { + userReport = text; + } + public static ErrorEventBuilder fromThrowable(Throwable t) { return builder().throwable(t).description(ExceptionConverter.convertMessage(t)); } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java index d3a8042a..74b8aa87 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -2,7 +2,6 @@ package io.xpipe.app.issue; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.TitledPaneComp; -import io.xpipe.app.core.App; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; @@ -10,6 +9,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.util.JfxHelper; +import io.xpipe.app.util.PlatformState; import javafx.application.Platform; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -44,7 +44,7 @@ public class ErrorHandlerComp extends SimpleComp { } public static void showAndWait(ErrorEvent event) { - if (!App.isPlatformRunning() || event.isOmitted()) { + if (PlatformState.getCurrent() != PlatformState.RUNNING || event.isOmitted()) { ErrorAction.ignore().handle(event); return; } diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java index 837785a6..dc120e10 100644 --- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java @@ -13,32 +13,40 @@ import java.nio.file.Files; import java.util.Date; import java.util.stream.Collectors; -public class SentryErrorHandler { +public class SentryErrorHandler implements ErrorHandler { - public static void init() { - AppProperties.init(); - if (AppProperties.get().getSentryUrl() != null) { - Sentry.init(options -> { - options.setDsn(AppProperties.get().getSentryUrl()); - options.setEnableUncaughtExceptionHandler(false); - options.setAttachServerName(false); - // options.setDebug(true); - options.setRelease(AppProperties.get().getVersion()); - options.setEnableShutdownHook(false); - options.setProguardUuid(AppProperties.get().getBuildUuid().toString()); - options.setTag("os", System.getProperty("os.name")); - options.setTag("osVersion", System.getProperty("os.version")); - options.setTag("arch", System.getProperty("os.arch")); - }); - } + private static final ErrorHandler INSTANCE = new SyncErrorHandler(new SentryErrorHandler()); - Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { - ErrorEvent.fromThrowable(ex).build().handle(); - }); + public static ErrorHandler getInstance() { + return INSTANCE; } - public static void report(ErrorEvent e, String text) { - var id = report(e); + private boolean init; + + public void handle(ErrorEvent ee) { + // Assume that this object is wrapped by a synchronous error handler + if (!init) { + AppProperties.init(); + if (AppProperties.get().getSentryUrl() != null) { + Sentry.init(options -> { + options.setDsn(AppProperties.get().getSentryUrl()); + options.setEnableUncaughtExceptionHandler(false); + options.setAttachServerName(false); + // options.setDebug(true); + options.setRelease(AppProperties.get().getVersion()); + options.setEnableShutdownHook(false); + options.setProguardUuid(AppProperties.get().getBuildUuid().toString()); + options.setTag("os", System.getProperty("os.name")); + options.setTag("osVersion", System.getProperty("os.version")); + options.setTag("arch", System.getProperty("os.arch")); + options.setDist(XPipeDistributionType.get().getId()); + }); + } + init = true; + } + + var id = createReport(ee); + var text = ee.getUserReport(); if (text != null && text.length() > 0) { var fb = new UserFeedback(id); fb.setComments(text); @@ -46,7 +54,7 @@ public class SentryErrorHandler { } } - public static SentryId report(ErrorEvent ee) { + private static SentryId createReport(ErrorEvent ee) { /* TODO: Ignore breadcrumbs for now */ @@ -86,7 +94,6 @@ public class SentryErrorHandler { .toList(); atts.forEach(attachment -> s.addAttachment(attachment)); - s.setTag("dist", XPipeDistributionType.get().getId()); s.setTag("developerMode", AppPrefs.get() != null ? AppPrefs.get().developerMode().getValue().toString() : "false"); s.setTag("terminal", Boolean.toString(ee.isTerminal())); s.setTag("omitted", Boolean.toString(ee.isOmitted())); @@ -97,7 +104,6 @@ public class SentryErrorHandler { } } - var user = new User(); user.setId(AppCache.getCachedUserId().toString()); s.setUser(user); diff --git a/app/src/main/java/io/xpipe/app/issue/SyncErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SyncErrorHandler.java new file mode 100644 index 00000000..5d70ee8e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/issue/SyncErrorHandler.java @@ -0,0 +1,38 @@ +package io.xpipe.app.issue; + +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SyncErrorHandler implements ErrorHandler { + + private final Queue eventQueue = new LinkedBlockingQueue<>(); + private final ErrorHandler errorHandler; + private final AtomicBoolean busy = new AtomicBoolean(); + + public SyncErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + @Override + public void handle(ErrorEvent event) { + synchronized (busy) { + if (busy.get()) { + synchronized (eventQueue) { + eventQueue.add(event); + } + return; + } + busy.set(true); + } + + errorHandler.handle(event); + synchronized (eventQueue) { + eventQueue.forEach(errorEvent -> { + System.out.println("Event happened during error handling: " + errorEvent.toString()); + }); + eventQueue.clear(); + } + busy.set(false); + } +} diff --git a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java index 89355795..1edd2eb8 100644 --- a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java @@ -5,6 +5,7 @@ import io.xpipe.app.core.*; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.update.XPipeDistributionType; +import io.xpipe.app.util.PlatformState; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; @@ -23,7 +24,7 @@ public class TerminalErrorHandler implements ErrorHandler { if (!OperationMode.GUI.isSupported()) { event.clearAttachments(); - SentryErrorHandler.report(event, null); + SentryErrorHandler.getInstance().handle(event); OperationMode.halt(1); } @@ -31,20 +32,20 @@ public class TerminalErrorHandler implements ErrorHandler { } private void handleSentry(ErrorEvent event) { - SentryErrorHandler.init(); if (OperationMode.isInStartup()) { Sentry.setExtra("initError", "true"); } } private void handleGui(ErrorEvent event) { - if (!App.isPlatformRunning()) { + if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) { try { CountDownLatch latch = new CountDownLatch(1); Platform.setImplicitExit(false); Platform.startup(latch::countDown); try { latch.await(); + PlatformState.setCurrent(PlatformState.RUNNING); } catch (InterruptedException ignored) { } } catch (Throwable r) { @@ -78,8 +79,8 @@ public class TerminalErrorHandler implements ErrorHandler { } private static void handleSecondaryException(ErrorEvent event, Throwable t) { - SentryErrorHandler.report(event, null); - SentryErrorHandler.report(ErrorEvent.fromThrowable(t).build(), null); + SentryErrorHandler.getInstance().handle(event); + SentryErrorHandler.getInstance().handle(ErrorEvent.fromThrowable(t).build()); Sentry.flush(5000); t.printStackTrace(); OperationMode.halt(1); @@ -106,7 +107,7 @@ public class TerminalErrorHandler implements ErrorHandler { } } } catch (Throwable t) { - SentryErrorHandler.report(ErrorEvent.fromThrowable(t).build(), null); + SentryErrorHandler.getInstance().handle(ErrorEvent.fromThrowable(t).build()); Sentry.flush(5000); t.printStackTrace(); OperationMode.halt(1); diff --git a/app/src/main/java/io/xpipe/app/issue/UserReportComp.java b/app/src/main/java/io/xpipe/app/issue/UserReportComp.java index 7a20c839..7cc3a86f 100644 --- a/app/src/main/java/io/xpipe/app/issue/UserReportComp.java +++ b/app/src/main/java/io/xpipe/app/issue/UserReportComp.java @@ -1,6 +1,5 @@ package io.xpipe.app.issue; -import io.sentry.Sentry; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ListSelectorComp; import io.xpipe.app.comp.base.TitledPaneComp; @@ -112,12 +111,10 @@ public class UserReportComp extends SimpleComp { } private void send() { - Sentry.withScope(scope -> { - event.clearAttachments(); - includedDiagnostics.forEach(event::addAttachment); - SentryErrorHandler.report(event, text.get()); - }); - + event.clearAttachments(); + includedDiagnostics.forEach(event::addAttachment); + event.attachUserReport(text.get()); + SentryErrorHandler.getInstance().handle(event); stage.close(); } } diff --git a/app/src/main/java/io/xpipe/app/util/PlatformState.java b/app/src/main/java/io/xpipe/app/util/PlatformState.java new file mode 100644 index 00000000..126c4f3d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/PlatformState.java @@ -0,0 +1,15 @@ +package io.xpipe.app.util; + +import lombok.Getter; +import lombok.Setter; + +public enum PlatformState { + + NOT_INITIALIZED, + RUNNING, + EXITED; + + @Getter + @Setter + private static PlatformState current = PlatformState.NOT_INITIALIZED; +}