mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 13:59:09 +02:00
fix(ui): install X11 error handler so tray races don't kill the UI
Xlib's default protocol-error handler calls exit() on any async X error, so a tray race (tray manager window dying between find and dock, XMoveWindow on a popup the WM already destroyed) would take the whole UI process down. Install process-global logging no-op handlers via XSetErrorHandler/ XSetIOErrorHandler after every XOpenDisplay. Since the handler slot is process-global and GDK init can clobber it, additionally wrap the popup code's raw Xlib calls on GDK's Display in gdk_x11_display_error_trap_push/ pop_ignored, which is independent of the global handler.
This commit is contained in:
@@ -5,4 +5,6 @@ Minimal WMs (Fluxbox, OpenBox, i3, dwm, vanilla GNOME without the AppIndicator e
|
|||||||
- `tray_watcher_linux.go` — owns `org.kde.StatusNotifierWatcher` on the session bus if no other process has it. Safe to call unconditionally.
|
- `tray_watcher_linux.go` — owns `org.kde.StatusNotifierWatcher` on the session bus if no other process has it. Safe to call unconditionally.
|
||||||
- `xembed_host_linux.go` + `xembed_tray_linux.{c,h}` — when an XEmbed tray (`_NET_SYSTEM_TRAY_S0`) is available, also start an in-process XEmbed host that bridges the SNI icon into the XEmbed tray. Reads `IconPixmap` over D-Bus, draws via cairo+X11, polls for clicks, fetches `com.canonical.dbusmenu.GetLayout` for the popup menu, fires `com.canonical.dbusmenu.Event` on click.
|
- `xembed_host_linux.go` + `xembed_tray_linux.{c,h}` — when an XEmbed tray (`_NET_SYSTEM_TRAY_S0`) is available, also start an in-process XEmbed host that bridges the SNI icon into the XEmbed tray. Reads `IconPixmap` over D-Bus, draws via cairo+X11, polls for clicks, fetches `com.canonical.dbusmenu.GetLayout` for the popup menu, fires `com.canonical.dbusmenu.Event` on click.
|
||||||
|
|
||||||
|
**X error handling.** Xlib's default protocol-error handler calls `exit()`, so a single async X error from a tray race (tray manager window dying between `xembed_find_tray` and `xembed_dock`; `XMoveWindow` on a popup the WM already destroyed) would take the whole UI process down. `xembed_install_error_handlers()` (`xembed_tray_linux.c`) installs process-global logging no-op handlers via `XSetErrorHandler`/`XSetIOErrorHandler`; it's idempotent and called from the Go side after every `XOpenDisplay` (`newXembedHost`, `xembedTrayAvailable`). Because the handler slot is process-global, GTK's GDK init can clobber it (whoever installs last wins) — so the popup code's raw Xlib calls **on GDK's Display** (`x11_move_window`, the submenu `XTranslateCoordinates`) are additionally wrapped in GDK's own `gdk_x11_display_error_trap_push`/`_pop_ignored`, which is independent of the global handler. (Those GDK X11 backend calls warn `-Wdeprecated-declarations` in GTK4, like the existing `gdk_x11_surface_get_xid`/`gdk_x11_display_get_xdisplay` calls — expected, still functional.)
|
||||||
|
|
||||||
Build is gated on `linux && !386`; the 386 build (no cgo) and non-Linux builds use the `tray_watcher_other.go` no-op.
|
Build is gated on `linux && !386`; the 386 build (no cgo) and non-Linux builds use the `tray_watcher_other.go` no-op.
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*x
|
|||||||
if dpy == nil {
|
if dpy == nil {
|
||||||
return nil, errors.New("cannot open X display")
|
return nil, errors.New("cannot open X display")
|
||||||
}
|
}
|
||||||
|
C.xembed_install_error_handlers()
|
||||||
|
|
||||||
screen := C.xembed_default_screen(dpy)
|
screen := C.xembed_default_screen(dpy)
|
||||||
trayMgr := C.xembed_find_tray(dpy, screen)
|
trayMgr := C.xembed_find_tray(dpy, screen)
|
||||||
@@ -383,6 +384,7 @@ func xembedTrayAvailable() bool {
|
|||||||
if dpy == nil {
|
if dpy == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
C.xembed_install_error_handlers()
|
||||||
defer C.XCloseDisplay(dpy)
|
defer C.XCloseDisplay(dpy)
|
||||||
screen := C.xembed_default_screen(dpy)
|
screen := C.xembed_default_screen(dpy)
|
||||||
return C.xembed_find_tray(dpy, screen) != 0
|
return C.xembed_find_tray(dpy, screen) != 0
|
||||||
|
|||||||
@@ -13,6 +13,41 @@
|
|||||||
#define SYSTEM_TRAY_REQUEST_DOCK 0
|
#define SYSTEM_TRAY_REQUEST_DOCK 0
|
||||||
#define XEMBED_MAPPED (1 << 0)
|
#define XEMBED_MAPPED (1 << 0)
|
||||||
|
|
||||||
|
/* Xlib's default protocol-error handler calls exit() on any async X error,
|
||||||
|
killing the whole UI process. Handlers are process-global (not
|
||||||
|
per-Display), so a single install covers our raw tray Display and GDK's.
|
||||||
|
Tray work is full of races the X server reports asynchronously — the tray
|
||||||
|
manager window dying between xembed_find_tray and xembed_dock (BadWindow
|
||||||
|
on XSendEvent), or x11_move_window touching a popup the WM already
|
||||||
|
destroyed — and the default handler would take us down for any of them.
|
||||||
|
Returning 0 here makes the error a logged no-op instead. */
|
||||||
|
static int xembed_x_error_handler(Display *dpy, XErrorEvent *ev) {
|
||||||
|
char buf[256];
|
||||||
|
XGetErrorText(dpy, ev->error_code, buf, sizeof(buf));
|
||||||
|
fprintf(stderr,
|
||||||
|
"xembed: X error (ignored): %s (code=%d, request=%d.%d, resource=0x%lx)\n",
|
||||||
|
buf, ev->error_code, ev->request_code, ev->minor_code,
|
||||||
|
ev->resourceid);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The I/O error handler fires when the X connection itself drops (server
|
||||||
|
gone, socket closed). Xlib treats this as fatal and exits even if we
|
||||||
|
return, so this can't keep the process alive — it only logs a clearer
|
||||||
|
line than Xlib's terse default before the unavoidable exit. */
|
||||||
|
static int xembed_x_io_error_handler(Display *dpy) {
|
||||||
|
(void)dpy;
|
||||||
|
fprintf(stderr, "xembed: X I/O error (connection lost)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Install the process-global handlers. Idempotent and cheap, so callers may
|
||||||
|
invoke it after every XOpenDisplay without tracking prior installs. */
|
||||||
|
void xembed_install_error_handlers(void) {
|
||||||
|
XSetErrorHandler(xembed_x_error_handler);
|
||||||
|
XSetIOErrorHandler(xembed_x_io_error_handler);
|
||||||
|
}
|
||||||
|
|
||||||
Window xembed_find_tray(Display *dpy, int screen) {
|
Window xembed_find_tray(Display *dpy, int screen) {
|
||||||
char atom_name[64];
|
char atom_name[64];
|
||||||
snprintf(atom_name, sizeof(atom_name), "_NET_SYSTEM_TRAY_S%d", screen);
|
snprintf(atom_name, sizeof(atom_name), "_NET_SYSTEM_TRAY_S%d", screen);
|
||||||
@@ -363,6 +398,14 @@ static void x11_move_window(GtkWidget *win, int x, int y) {
|
|||||||
GdkDisplay *display = gdk_surface_get_display(surface);
|
GdkDisplay *display = gdk_surface_get_display(surface);
|
||||||
Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display));
|
Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display));
|
||||||
|
|
||||||
|
/* These calls poke a window the WM may have already destroyed (a popup
|
||||||
|
torn down between scheduling and this idle callback). On GDK's Display
|
||||||
|
use GDK's own error trap rather than our global handler — push/pop is
|
||||||
|
the spec-correct way to make untrapped BadWindow/BadMatch from these
|
||||||
|
raw Xlib calls non-fatal, independent of whichever process-global
|
||||||
|
handler happens to be installed. */
|
||||||
|
gdk_x11_display_error_trap_push(display);
|
||||||
|
|
||||||
/* _NET_WM_WINDOW_TYPE_POPUP_MENU: makes fluxbox / openbox / etc
|
/* _NET_WM_WINDOW_TYPE_POPUP_MENU: makes fluxbox / openbox / etc
|
||||||
render the window above panels and skip decorations. Must be
|
render the window above panels and skip decorations. Must be
|
||||||
set before the window is mapped to be honoured by some WMs;
|
set before the window is mapped to be honoured by some WMs;
|
||||||
@@ -409,6 +452,7 @@ static void x11_move_window(GtkWidget *win, int x, int y) {
|
|||||||
(XEvent *)&ev);
|
(XEvent *)&ev);
|
||||||
|
|
||||||
XFlush(xdpy);
|
XFlush(xdpy);
|
||||||
|
gdk_x11_display_error_trap_pop_ignored(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forward declaration — submenu buttons need to schedule a child popup. */
|
/* Forward declaration — submenu buttons need to schedule a child popup. */
|
||||||
@@ -453,8 +497,12 @@ static void on_submenu_button_clicked(GtkButton *btn, gpointer user_data) {
|
|||||||
GdkDisplay *display = gdk_surface_get_display(anchor_surface);
|
GdkDisplay *display = gdk_surface_get_display(anchor_surface);
|
||||||
Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display));
|
Display *xdpy = gdk_x11_display_get_xdisplay(GDK_X11_DISPLAY(display));
|
||||||
Window child;
|
Window child;
|
||||||
|
/* Trap BadWindow in case the anchor's surface is torn down between
|
||||||
|
the click and this handler running. */
|
||||||
|
gdk_x11_display_error_trap_push(display);
|
||||||
XTranslateCoordinates(xdpy, axid, DefaultRootWindow(xdpy),
|
XTranslateCoordinates(xdpy, axid, DefaultRootWindow(xdpy),
|
||||||
0, 0, &ox, &oy, &child);
|
0, 0, &ox, &oy, &child);
|
||||||
|
gdk_x11_display_error_trap_pop_ignored(display);
|
||||||
}
|
}
|
||||||
int ax = ox + (int)bounds.origin.x;
|
int ax = ox + (int)bounds.origin.x;
|
||||||
int ay = oy + (int)bounds.origin.y;
|
int ay = oy + (int)bounds.origin.y;
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ static inline int xembed_default_screen(Display *dpy) {
|
|||||||
return DefaultScreen(dpy);
|
return DefaultScreen(dpy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xembed_install_error_handlers replaces Xlib's default protocol- and
|
||||||
|
// I/O-error handlers (which call exit()) with logging handlers, so an async
|
||||||
|
// X error from a tray race doesn't kill the UI process. Process-global and
|
||||||
|
// idempotent; safe to call after every XOpenDisplay.
|
||||||
|
void xembed_install_error_handlers(void);
|
||||||
|
|
||||||
// xembed_find_tray returns the selection owner window for
|
// xembed_find_tray returns the selection owner window for
|
||||||
// _NET_SYSTEM_TRAY_S{screen}, or 0 if no XEmbed tray manager exists.
|
// _NET_SYSTEM_TRAY_S{screen}, or 0 if no XEmbed tray manager exists.
|
||||||
Window xembed_find_tray(Display *dpy, int screen);
|
Window xembed_find_tray(Display *dpy, int screen);
|
||||||
|
|||||||
Reference in New Issue
Block a user