Updated GTK threading model to *correctly* execute GTK methods on the
GTK thread. Solves problems (unsurprisingly) with race conditions against the native GTK thread.
This commit is contained in:
parent
71339e3267
commit
189ec064fa
|
@ -79,13 +79,11 @@ class GtkMenuEntry implements MenuEntry, GCallback {
|
||||||
int callback(final Pointer instance, final Pointer data) {
|
int callback(final Pointer instance, final Pointer data) {
|
||||||
final SystemTrayMenuAction cb = this.callback;
|
final SystemTrayMenuAction cb = this.callback;
|
||||||
if (cb != null) {
|
if (cb != null) {
|
||||||
GtkTypeSystemTray.callbackExecutor.execute(new Runnable() {
|
try {
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
cb.onClick(parent, GtkMenuEntry.this);
|
cb.onClick(parent, GtkMenuEntry.this);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Gtk.TRUE;
|
return Gtk.TRUE;
|
||||||
|
|
|
@ -21,6 +21,8 @@ import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
import dorkbox.systemTray.linux.jna.Gtk;
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
import dorkbox.systemTray.linux.jna.GtkSupport;
|
import dorkbox.systemTray.linux.jna.GtkSupport;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,11 +34,8 @@ public
|
||||||
class GtkSystemTray extends GtkTypeSystemTray {
|
class GtkSystemTray extends GtkTypeSystemTray {
|
||||||
private Pointer trayIcon;
|
private Pointer trayIcon;
|
||||||
|
|
||||||
// have to make this a field, to prevent GC on this object
|
// have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
|
||||||
@SuppressWarnings("FieldCanBeLocal")
|
private final List<Object> gtkCallbacks = new ArrayList<Object>();
|
||||||
private Gobject.GEventCallback gtkCallback;
|
|
||||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
|
||||||
private NativeLong button_press_event;
|
|
||||||
|
|
||||||
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
|
||||||
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
private AtomicBoolean shuttingDown = new AtomicBoolean();
|
||||||
|
@ -53,12 +52,12 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
final Pointer trayIcon_ = gtk.gtk_status_icon_new();
|
final Pointer trayIcon_ = gtk.gtk_status_icon_new();
|
||||||
gtk.gtk_status_icon_set_title(trayIcon_, "SystemTray@Dorkbox");
|
gtk.gtk_status_icon_set_title(trayIcon_, GnomeShellExtension.UID);
|
||||||
gtk.gtk_status_icon_set_name(trayIcon_, "SystemTray");
|
gtk.gtk_status_icon_set_name(trayIcon_, "SystemTray");
|
||||||
|
|
||||||
trayIcon = trayIcon_;
|
trayIcon = trayIcon_;
|
||||||
|
|
||||||
gtkCallback = new Gobject.GEventCallback() {
|
final Gobject.GEventCallback gtkCallback = new Gobject.GEventCallback() {
|
||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void callback(Pointer notUsed, final Gtk.GdkEventButton event) {
|
void callback(Pointer notUsed, final Gtk.GdkEventButton event) {
|
||||||
|
@ -68,7 +67,11 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
button_press_event = gobject.g_signal_connect_object(trayIcon, "button_press_event", gtkCallback, null, 0);
|
final NativeLong button_press_event = gobject.g_signal_connect_object(trayIcon, "button_press_event", gtkCallback, null, 0);
|
||||||
|
|
||||||
|
// have to do this to prevent GC on these objects
|
||||||
|
gtkCallbacks.add(gtkCallback);
|
||||||
|
gtkCallbacks.add(button_press_event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -86,9 +89,9 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
||||||
gtk.gtk_status_icon_set_visible(trayIcon, false);
|
gtk.gtk_status_icon_set_visible(trayIcon, false);
|
||||||
gobject.g_object_unref(trayIcon);
|
gobject.g_object_unref(trayIcon);
|
||||||
|
|
||||||
// GC it
|
// mark for GC
|
||||||
trayIcon = null;
|
trayIcon = null;
|
||||||
gtkCallback = null;
|
gtkCallbacks.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,9 @@ import dorkbox.systemTray.SystemTrayMenuAction;
|
||||||
import dorkbox.systemTray.linux.jna.Gobject;
|
import dorkbox.systemTray.linux.jna.Gobject;
|
||||||
import dorkbox.systemTray.linux.jna.Gtk;
|
import dorkbox.systemTray.linux.jna.Gtk;
|
||||||
import dorkbox.systemTray.linux.jna.GtkSupport;
|
import dorkbox.systemTray.linux.jna.GtkSupport;
|
||||||
import dorkbox.util.NamedThreadFactory;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derived from
|
* Derived from
|
||||||
|
@ -40,8 +36,6 @@ class GtkTypeSystemTray extends SystemTray {
|
||||||
protected static final Gobject gobject = Gobject.INSTANCE;
|
protected static final Gobject gobject = Gobject.INSTANCE;
|
||||||
protected static final Gtk gtk = Gtk.INSTANCE;
|
protected static final Gtk gtk = Gtk.INSTANCE;
|
||||||
|
|
||||||
final static ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("SysTrayExecutor", false));
|
|
||||||
|
|
||||||
private volatile Pointer menu;
|
private volatile Pointer menu;
|
||||||
|
|
||||||
private volatile Pointer connectionStatusItem;
|
private volatile Pointer connectionStatusItem;
|
||||||
|
@ -62,16 +56,6 @@ class GtkTypeSystemTray extends SystemTray {
|
||||||
void run() {
|
void run() {
|
||||||
obliterateMenu();
|
obliterateMenu();
|
||||||
|
|
||||||
boolean terminated = false;
|
|
||||||
try {
|
|
||||||
terminated = callbackExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!terminated) {
|
|
||||||
callbackExecutor.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
GtkSupport.shutdownGui();
|
GtkSupport.shutdownGui();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,9 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package dorkbox.systemTray.linux.jna;
|
package dorkbox.systemTray.linux.jna;
|
||||||
|
|
||||||
import com.sun.jna.*;
|
import com.sun.jna.Callback;
|
||||||
|
import com.sun.jna.Library;
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.NativeLong;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.Structure;
|
||||||
import dorkbox.util.Keep;
|
import dorkbox.util.Keep;
|
||||||
import dorkbox.systemTray.linux.jna.Gtk.GdkEventButton;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -126,6 +130,14 @@ interface Gobject extends Library {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
interface FuncCallback extends Callback {
|
||||||
|
/**
|
||||||
|
* @return Gtk.FALSE if it will be automatically removed from the stack once it's handled
|
||||||
|
*/
|
||||||
|
int callback(Pointer data);
|
||||||
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
interface GCallback extends Callback {
|
interface GCallback extends Callback {
|
||||||
/**
|
/**
|
||||||
|
@ -137,7 +149,7 @@ interface Gobject extends Library {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
interface GEventCallback extends Callback {
|
interface GEventCallback extends Callback {
|
||||||
void callback(Pointer instance, GdkEventButton event);
|
void callback(Pointer instance, Gtk.GdkEventButton event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,15 @@ interface Gtk extends Library {
|
||||||
*/
|
*/
|
||||||
void gtk_main();
|
void gtk_main();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* using g_idle_add() instead would require thread protection in the callback
|
||||||
|
* @param callback
|
||||||
|
* @param data
|
||||||
|
* @return TRUE to run this callback again, FALSE to remove from the list of event sources (and not call it again)
|
||||||
|
*/
|
||||||
|
int gdk_threads_add_idle (Gobject.FuncCallback callback, Pointer data);
|
||||||
|
|
||||||
/** sks for the current nesting level of the main loop. Useful to determine (at startup) if GTK is already runnign */
|
/** aks for the current nesting level of the main loop. Useful to determine (at startup) if GTK is already running */
|
||||||
int gtk_main_level();
|
int gtk_main_level();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,23 +17,31 @@ package dorkbox.systemTray.linux.jna;
|
||||||
|
|
||||||
import com.sun.jna.Function;
|
import com.sun.jna.Function;
|
||||||
import com.sun.jna.Native;
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
import dorkbox.systemTray.SystemTray;
|
import dorkbox.systemTray.SystemTray;
|
||||||
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.LinkedList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
public
|
public
|
||||||
class GtkSupport {
|
class GtkSupport {
|
||||||
|
// For funsies, SyncThing did a LOT of work on compatibility (unfortunate for us) in python.
|
||||||
|
// https://github.com/syncthing/syncthing-gtk/blob/b7a3bc00e3bb6d62365ae62b5395370f3dcc7f55/syncthing_gtk/statusicon.py
|
||||||
|
|
||||||
private static volatile boolean started = false;
|
private static volatile boolean started = false;
|
||||||
private static final ArrayBlockingQueue<Runnable> dispatchEvents = new ArrayBlockingQueue<Runnable>(256);
|
|
||||||
private static volatile Thread gtkDispatchThread;
|
// have to save these in a field to prevent GC on the objects (since they go out-of-scope from java)
|
||||||
|
private static final LinkedList<Object> gtkCallbacks = new LinkedList<Object>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* must call get() before accessing this! Only "Gtk" interface should access this!
|
* must call get() before accessing this! Only "Gtk" interface should access this!
|
||||||
*/
|
*/
|
||||||
static volatile Function gtk_status_icon_position_menu = null;
|
static volatile Function gtk_status_icon_position_menu = null;
|
||||||
|
|
||||||
public static volatile boolean isGtk2 = false;
|
public static volatile boolean isGtk2 = false;
|
||||||
|
|
||||||
private static volatile boolean alreadyRunningGTK = false;
|
private static volatile boolean alreadyRunningGTK = false;
|
||||||
|
private static Thread gtkUpdateThread = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for GTK, because we could have v3 or v2.
|
* Helper for GTK, because we could have v3 or v2.
|
||||||
|
@ -130,7 +138,7 @@ class GtkSupport {
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("We apologize for this, but we are unable to determine the GTK library is in use, if " +
|
throw new RuntimeException("We apologize for this, but we are unable to determine the GTK library is in use, " +
|
||||||
"or even if it is in use... Please create an issue for this and include your OS type and configuration.");
|
"or even if it is in use... Please create an issue for this and include your OS type and configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,43 +148,14 @@ class GtkSupport {
|
||||||
if (!started) {
|
if (!started) {
|
||||||
started = true;
|
started = true;
|
||||||
|
|
||||||
// GTK specifies that we ONLY run from a single thread. This guarantees that.
|
|
||||||
gtkDispatchThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
final Gtk gtk = Gtk.INSTANCE;
|
|
||||||
while (started) {
|
|
||||||
try {
|
|
||||||
final Runnable take = dispatchEvents.take();
|
|
||||||
|
|
||||||
gtk.gdk_threads_enter();
|
|
||||||
|
|
||||||
try {
|
|
||||||
take.run();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
gtk.gdk_threads_leave();
|
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gtkDispatchThread.setName("GTK Event Loop");
|
|
||||||
gtkDispatchThread.start();
|
|
||||||
|
|
||||||
|
|
||||||
// startup the GTK GUI event loop. There can be multiple/nested loops.
|
// startup the GTK GUI event loop. There can be multiple/nested loops.
|
||||||
|
|
||||||
// If JavaFX/SWT is used, this is UNNECESSARY
|
// If JavaFX/SWT is used, this is UNNECESSARY
|
||||||
if (!alreadyRunningGTK) {
|
if (!alreadyRunningGTK) {
|
||||||
// only necessary if we are the only GTK instance running...
|
// only necessary if we are the only GTK instance running...
|
||||||
final CountDownLatch blockUntilStarted = new CountDownLatch(1);
|
final CountDownLatch blockUntilStarted = new CountDownLatch(1);
|
||||||
Thread gtkUpdateThread = new Thread() {
|
|
||||||
|
gtkUpdateThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
|
@ -191,6 +170,7 @@ class GtkSupport {
|
||||||
if (!SystemTray.COMPATIBILITY_MODE) {
|
if (!SystemTray.COMPATIBILITY_MODE) {
|
||||||
gtk.gtk_init_check(0, null);
|
gtk.gtk_init_check(0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify our main thread to continue
|
// notify our main thread to continue
|
||||||
blockUntilStarted.countDown();
|
blockUntilStarted.countDown();
|
||||||
|
|
||||||
|
@ -202,7 +182,7 @@ class GtkSupport {
|
||||||
gtk.gdk_threads_leave();
|
gtk.gdk_threads_leave();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
gtkUpdateThread.setName("GTK Event Loop (Native)");
|
gtkUpdateThread.setName("GTK Native Event Loop");
|
||||||
gtkUpdateThread.start();
|
gtkUpdateThread.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -216,40 +196,41 @@ class GtkSupport {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Best practices for GTK, is to call EVERYTHING for it on a SINGLE THREAD. This accomplishes that.
|
* Best practices for GTK, is to call EVERYTHING for it on the GTK THREAD. This accomplishes that.
|
||||||
*/
|
*/
|
||||||
public static
|
public static
|
||||||
void dispatch(Runnable runnable) {
|
void dispatch(final Runnable runnable) {
|
||||||
try {
|
if (gtkUpdateThread == Thread.currentThread()) {
|
||||||
dispatchEvents.put(runnable);
|
// if we are ALREADY inside the native event
|
||||||
} catch (InterruptedException e) {
|
runnable.run();
|
||||||
e.printStackTrace();
|
} else {
|
||||||
|
final Gobject.FuncCallback callback = new Gobject.FuncCallback() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
int callback(final Pointer data) {
|
||||||
|
synchronized (gtkCallbacks) {
|
||||||
|
gtkCallbacks.removeFirst(); // now that we've 'handled' it, we can remove it from our callback list
|
||||||
|
}
|
||||||
|
runnable.run();
|
||||||
|
|
||||||
|
return Gtk.FALSE; // don't want to call this again
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
synchronized (gtkCallbacks) {
|
||||||
|
gtkCallbacks.offer(callback); // prevent GC from collecting this object before it can be called
|
||||||
|
}
|
||||||
|
Gtk.INSTANCE.gdk_threads_add_idle(callback, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static
|
public static
|
||||||
void shutdownGui() {
|
void shutdownGui() {
|
||||||
// If JavaFX/SWT is used, this is UNNECESSARY (an will break SWT/JavaFX shutdown)
|
// If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown)
|
||||||
if (!(alreadyRunningGTK || SystemTray.COMPATIBILITY_MODE)) {
|
if (!(alreadyRunningGTK || SystemTray.COMPATIBILITY_MODE)) {
|
||||||
Gtk.INSTANCE.gtk_main_quit();
|
Gtk.INSTANCE.gtk_main_quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
started = false;
|
started = false;
|
||||||
|
|
||||||
// put it in a NEW dispatch event (so that we cleanup AFTER this one is finished)
|
|
||||||
dispatch(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public
|
|
||||||
void run() {
|
|
||||||
// this should happen in a new thread
|
|
||||||
gtkDispatchThread.interrupt();
|
|
||||||
}
|
|
||||||
}).run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user