mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-22 15:40:31 -04:00
Rework exit on windows
This commit is contained in:
88
app/src/main/java/io/xpipe/app/core/AppWindowsShutdown.java
Normal file
88
app/src/main/java/io/xpipe/app/core/AppWindowsShutdown.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package io.xpipe.app.core;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.platform.win32.User32;
|
||||
import com.sun.jna.platform.win32.WinDef;
|
||||
import com.sun.jna.platform.win32.WinUser;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AppWindowsShutdown {
|
||||
|
||||
// Prevent GC
|
||||
private static final WinShutdownHookProc PROC = new WinShutdownHookProc();
|
||||
|
||||
public static void registerHook(WinDef.HWND hwnd) {
|
||||
int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);
|
||||
if (windowThreadID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PROC.hwnd = hwnd;
|
||||
PROC.hhook = User32.INSTANCE.SetWindowsHookEx(4, PROC, null, windowThreadID);
|
||||
}
|
||||
|
||||
public static class CWPSSTRUCT extends Structure {
|
||||
public WinDef.LPARAM lParam;
|
||||
public WinDef.WPARAM wParam;
|
||||
public WinDef.DWORD message;
|
||||
public WinDef.HWND hwnd;
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return List.of("lParam", "wParam", "message", "hwnd");
|
||||
}
|
||||
}
|
||||
|
||||
public interface WinHookProc extends WinUser.HOOKPROC {
|
||||
|
||||
WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct);
|
||||
}
|
||||
|
||||
public static final int WM_ENDSESSION = 0x16;
|
||||
public static final int WM_QUERYENDSESSION = 0x11;
|
||||
public static final long ENDSESSION_CRITICAL = 0x40000000L;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static final class WinShutdownHookProc implements WinHookProc {
|
||||
|
||||
@Setter
|
||||
private WinUser.HHOOK hhook;
|
||||
@Setter
|
||||
private WinDef.HWND hwnd;
|
||||
|
||||
@Override
|
||||
public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct) {
|
||||
if (nCode >= 0 && hookProcStruct.hwnd.equals(hwnd)) {
|
||||
if (hookProcStruct.message.longValue() == WM_QUERYENDSESSION) {
|
||||
// Indicates that we need to run the endsession case blocking
|
||||
return new WinDef.LRESULT(0);
|
||||
}
|
||||
|
||||
if (hookProcStruct.message.longValue() == WM_ENDSESSION) {
|
||||
// Instant exit for critical shutdowns
|
||||
if (hookProcStruct.lParam.longValue() == ENDSESSION_CRITICAL) {
|
||||
OperationMode.halt(0);
|
||||
}
|
||||
|
||||
// A shutdown hook will be started in parallel while we exit
|
||||
// The only thing we have to do is wait for it to exit the platform
|
||||
while (PlatformState.getCurrent() != PlatformState.EXITED) {
|
||||
ThreadHelper.sleep(100);
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
}
|
||||
|
||||
return new WinDef.LRESULT(0);
|
||||
}
|
||||
}
|
||||
return User32.INSTANCE.CallNextHookEx(hhook, nCode, wParam, new WinDef.LPARAM(Pointer.nativeValue(hookProcStruct.getPointer())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class GuiMode extends PlatformMode {
|
||||
|
||||
@@ -19,6 +19,7 @@ import javafx.application.Platform;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class OperationMode {
|
||||
@@ -34,9 +35,6 @@ public abstract class OperationMode {
|
||||
@Getter
|
||||
private static boolean inShutdown;
|
||||
|
||||
@Getter
|
||||
private static boolean inShutdownHook;
|
||||
|
||||
private static OperationMode CURRENT = null;
|
||||
|
||||
public static OperationMode map(XPipeDaemonMode mode) {
|
||||
@@ -73,7 +71,7 @@ public abstract class OperationMode {
|
||||
}
|
||||
|
||||
TrackEvent.info("Received SIGTERM externally");
|
||||
OperationMode.shutdown(true, false);
|
||||
OperationMode.shutdown(false);
|
||||
}));
|
||||
|
||||
// Handle uncaught exceptions
|
||||
@@ -174,7 +172,7 @@ public abstract class OperationMode {
|
||||
if (OsType.getLocal() != OsType.LINUX) {
|
||||
OperationMode.switchToSyncOrThrow(OperationMode.GUI);
|
||||
}
|
||||
OperationMode.shutdown(false, false);
|
||||
OperationMode.shutdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,7 +273,6 @@ public abstract class OperationMode {
|
||||
}
|
||||
|
||||
inShutdown = true;
|
||||
inShutdownHook = false;
|
||||
try {
|
||||
if (CURRENT != null) {
|
||||
CURRENT.finalTeardown();
|
||||
@@ -320,35 +317,20 @@ public abstract class OperationMode {
|
||||
});
|
||||
}
|
||||
|
||||
public static void shutdown(boolean inShutdownHook, boolean hasError) {
|
||||
@SneakyThrows
|
||||
public static void shutdown(boolean hasError) {
|
||||
if (isInStartup()) {
|
||||
TrackEvent.info("Received shutdown request while in startup. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
// In case we are stuck while in shutdown, instantly exit this application
|
||||
if (inShutdown && inShutdownHook) {
|
||||
TrackEvent.info("Received another shutdown request while in shutdown hook. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
if (inShutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run a timer to always exit after some time in case we get stuck
|
||||
if (!hasError && !AppProperties.get().isDevelopmentEnvironment()) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ThreadHelper.sleep(25000);
|
||||
TrackEvent.info("Shutdown took too long. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
});
|
||||
}
|
||||
|
||||
TrackEvent.info("Starting shutdown ...");
|
||||
|
||||
inShutdown = true;
|
||||
OperationMode.inShutdownHook = inShutdownHook;
|
||||
// Keep a non-daemon thread running
|
||||
var thread = ThreadHelper.createPlatformThread("shutdown", false, () -> {
|
||||
try {
|
||||
@@ -364,6 +346,14 @@ public abstract class OperationMode {
|
||||
OperationMode.halt(hasError ? 1 : 0);
|
||||
});
|
||||
thread.start();
|
||||
|
||||
// Use a timer to always exit after some time in case we get stuck
|
||||
var limit = !hasError && !AppProperties.get().isDevelopmentEnvironment() ? 25000 : Integer.MAX_VALUE;
|
||||
var exited = thread.join(Duration.ofMillis(limit));
|
||||
if (!exited) {
|
||||
TrackEvent.info("Shutdown took too long. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void set(OperationMode newMode) {
|
||||
@@ -381,7 +371,7 @@ public abstract class OperationMode {
|
||||
|
||||
try {
|
||||
if (newMode == null) {
|
||||
shutdown(false, false);
|
||||
shutdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public class AppMainWindow {
|
||||
@Getter
|
||||
private static final Property<String> loadingText = new SimpleObjectProperty<>();
|
||||
|
||||
private boolean shown = false;
|
||||
|
||||
private AppMainWindow(Stage stage) {
|
||||
this.stage = stage;
|
||||
}
|
||||
@@ -145,9 +147,12 @@ public class AppMainWindow {
|
||||
|
||||
public void show() {
|
||||
stage.show();
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
NativeWinWindowControl.MAIN_WINDOW = new NativeWinWindowControl(stage);
|
||||
if (OsType.getLocal() == OsType.WINDOWS && !shown) {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
NativeWinWindowControl.MAIN_WINDOW = ctrl;
|
||||
AppWindowsShutdown.registerHook(ctrl.getWindowHandle());
|
||||
}
|
||||
shown = true;
|
||||
}
|
||||
|
||||
private static String createTitle() {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package io.xpipe.app.core.window;
|
||||
|
||||
import io.xpipe.app.core.AppLogs;
|
||||
import io.xpipe.app.core.AppWindowsShutdown;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
@@ -14,8 +19,11 @@ import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public class ModifiedStage extends Stage {
|
||||
|
||||
public static boolean mergeFrame() {
|
||||
@@ -55,6 +63,7 @@ public class ModifiedStage extends Stage {
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static void applyModes(Stage stage) {
|
||||
if (stage.getScene() == null) {
|
||||
return;
|
||||
@@ -71,47 +80,40 @@ public class ModifiedStage extends Stage {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {}
|
||||
case OsType.MacOs macOs -> {
|
||||
var ctrl = new NativeMacOsWindowControl(stage);
|
||||
var seamlessFrame = AppMainWindow.getInstance() != null
|
||||
&& AppMainWindow.getInstance().getStage() == stage
|
||||
&& !AppPrefs.get().performanceMode().get()
|
||||
&& mergeFrame();
|
||||
var seamlessFrameApplied = ctrl.setAppearance(
|
||||
seamlessFrame, AppPrefs.get().theme().getValue().isDark())
|
||||
&& seamlessFrame;
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
ctrl.setWindowAttribute(
|
||||
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
||||
AppPrefs.get().theme().getValue().isDark());
|
||||
boolean seamlessFrame;
|
||||
if (AppPrefs.get().performanceMode().get()
|
||||
|| !mergeFrame()
|
||||
|| AppMainWindow.getInstance() == null
|
||||
|| stage != AppMainWindow.getInstance().getStage()) {
|
||||
seamlessFrame = false;
|
||||
} else {
|
||||
// This is not available on Windows 10
|
||||
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT)
|
||||
|| SystemUtils.IS_OS_WINDOWS_10;
|
||||
try {
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
var ctrl = new NativeMacOsWindowControl(stage);
|
||||
var seamlessFrame = AppMainWindow.getInstance() != null &&
|
||||
AppMainWindow.getInstance().getStage() == stage &&
|
||||
!AppPrefs.get().performanceMode().get() &&
|
||||
mergeFrame();
|
||||
var seamlessFrameApplied = ctrl.setAppearance(seamlessFrame, AppPrefs.get().theme().getValue().isDark()) && seamlessFrame;
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
ctrl.setWindowAttribute(NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
||||
AppPrefs.get().theme().getValue().isDark());
|
||||
boolean seamlessFrame;
|
||||
if (AppPrefs.get().performanceMode().get() ||
|
||||
!mergeFrame() ||
|
||||
AppMainWindow.getInstance() == null ||
|
||||
stage != AppMainWindow.getInstance().getStage()) {
|
||||
seamlessFrame = false;
|
||||
} else {
|
||||
// This is not available on Windows 10
|
||||
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT) || SystemUtils.IS_OS_WINDOWS_10;
|
||||
}
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
|
||||
}
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
ErrorEvent.fromThrowable(t).omit().handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,21 @@ import java.util.List;
|
||||
@EqualsAndHashCode
|
||||
public class NativeWinWindowControl {
|
||||
|
||||
@SneakyThrows
|
||||
public static WinDef.HWND byWindow(Window window) {
|
||||
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
|
||||
tkStageGetter.setAccessible(true);
|
||||
Object tkStage = tkStageGetter.invoke(window);
|
||||
Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
|
||||
getPlatformWindow.setAccessible(true);
|
||||
Object platformWindow = getPlatformWindow.invoke(tkStage);
|
||||
Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
|
||||
getNativeHandle.setAccessible(true);
|
||||
Object nativeHandle = getNativeHandle.invoke(platformWindow);
|
||||
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
|
||||
return hwnd;
|
||||
}
|
||||
|
||||
public static List<NativeWinWindowControl> byPid(long pid) {
|
||||
var refs = new ArrayList<NativeWinWindowControl>();
|
||||
User32.INSTANCE.EnumWindows(
|
||||
@@ -50,17 +65,7 @@ public class NativeWinWindowControl {
|
||||
|
||||
@SneakyThrows
|
||||
public NativeWinWindowControl(Window stage) {
|
||||
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
|
||||
tkStageGetter.setAccessible(true);
|
||||
Object tkStage = tkStageGetter.invoke(stage);
|
||||
Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
|
||||
getPlatformWindow.setAccessible(true);
|
||||
Object platformWindow = getPlatformWindow.invoke(tkStage);
|
||||
Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
|
||||
getNativeHandle.setAccessible(true);
|
||||
Object nativeHandle = getNativeHandle.invoke(platformWindow);
|
||||
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
|
||||
this.windowHandle = hwnd;
|
||||
this.windowHandle = byWindow(stage);
|
||||
}
|
||||
|
||||
public NativeWinWindowControl(WinDef.HWND windowHandle) {
|
||||
|
||||
@@ -10,7 +10,7 @@ public enum CloseBehaviour implements PrefsChoiceValue {
|
||||
QUIT("app.quit") {
|
||||
@Override
|
||||
public void run() {
|
||||
OperationMode.shutdown(false, false);
|
||||
OperationMode.shutdown(false);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.core.AppWindowsShutdown;
|
||||
import io.xpipe.app.core.check.AppSystemFontCheck;
|
||||
import io.xpipe.app.core.window.ModifiedStage;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
@@ -11,6 +12,7 @@ import javafx.scene.text.Font;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -103,6 +105,9 @@ public enum PlatformState {
|
||||
// Check if we have no fonts and set properties to load bundled ones
|
||||
AppSystemFontCheck.init();
|
||||
|
||||
// We use our own shutdown hook
|
||||
disableToolkitShutdownHook();
|
||||
|
||||
if (AppPrefs.get() != null) {
|
||||
var s = AppPrefs.get().uiScale().getValue();
|
||||
if (s != null) {
|
||||
@@ -205,4 +210,16 @@ public enum PlatformState {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static void disableToolkitShutdownHook() {
|
||||
var tkClass = Class.forName(ModuleLayer.boot().findModule("javafx.graphics").orElseThrow(), "com.sun.javafx.tk.Toolkit");
|
||||
var getToolkitMethod = tkClass.getDeclaredMethod("getToolkit");
|
||||
getToolkitMethod.setAccessible(true);
|
||||
var tk = getToolkitMethod.invoke(null);
|
||||
var shutdownHookField = tk.getClass().getDeclaredField("shutdownHook");
|
||||
shutdownHookField.setAccessible(true);
|
||||
var thread = (Thread) shutdownHookField.get(tk);
|
||||
Runtime.getRuntime().removeShutdownHook(thread);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +266,7 @@ public class PlatformThread {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once the shutdown hooks are run, the toolkit is shutdown, causing it to no longer perform runLater operations
|
||||
if (OperationMode.isInShutdownHook()) {
|
||||
if (OperationMode.isInShutdown()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ project.ext {
|
||||
"--add-exports", "jdk.zipfs/jdk.nio.zipfs=io.xpipe.modulefs",
|
||||
"--add-opens", "javafx.graphics/com.sun.glass.ui=io.xpipe.app",
|
||||
"--add-opens", "javafx.graphics/javafx.stage=io.xpipe.app",
|
||||
"--add-opens", "javafx.graphics/com.sun.javafx.tk=io.xpipe.app",
|
||||
"--add-opens", "javafx.graphics/com.sun.javafx.tk.quantum=io.xpipe.app",
|
||||
"-Dio.xpipe.app.arch=$rootProject.arch",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
|
||||
Reference in New Issue
Block a user