Skip to content

Commit ab7f780

Browse files
committed
Improve headlessness detection for backend selection.
We currently check the $DISPLAY environment variable to autodetect whether we should auto-pick a non-interactive backend on Linux, but that variable can be set to an "invalid" value. A realistic use case is for example a tmux session started interactively inheriting an initially valid $DISPLAY, but to which one later reconnects e.g. via ssh, at which point $DISPLAY becomes invalid. Before this PR, something like ``` DISPLAY=:123 MPLBACKEND= MATPLOTLIBRC=/dev/null python -c 'import pylab' ``` (where we unset matplotlibrc to force backend autoselection) would crash when we select qt and qt fails to initialize as $DISPLAY is invalid (qt unconditionally abort()s via qFatal() in that case). With this PR, we correctly autoselect a non-interactive backend.
1 parent 3d725f6 commit ab7f780

File tree

5 files changed

+77
-16
lines changed

5 files changed

+77
-16
lines changed

lib/matplotlib/backend_bases.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -2755,10 +2755,15 @@ def show(self):
27552755
warning in `.Figure.show`.
27562756
"""
27572757
# This should be overridden in GUI backends.
2758-
if cbook._get_running_interactive_framework() != "headless":
2759-
raise NonGuiException(
2760-
f"Matplotlib is currently using {get_backend()}, which is "
2761-
f"a non-GUI backend, so cannot show the figure.")
2758+
if sys.platform == "linux" and not os.environ.get("DISPLAY"):
2759+
# We cannot check _get_running_interactive_framework() ==
2760+
# "headless" because that would also suppress the warning when
2761+
# $DISPLAY exists but is invalid, which is more likely an error and
2762+
# thus warrants a warning.
2763+
return
2764+
raise NonGuiException(
2765+
f"Matplotlib is currently using {get_backend()}, which is a "
2766+
f"non-GUI backend, so cannot show the figure.")
27622767

27632768
def destroy(self):
27642769
pass

lib/matplotlib/backends/backend_qt5.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import traceback
88

99
import matplotlib
10-
1110
from matplotlib import backend_tools, cbook
1211
from matplotlib._pylab_helpers import Gcf
1312
from matplotlib.backend_bases import (
@@ -108,15 +107,13 @@ def _create_qApp():
108107
importlib.import_module(
109108
# i.e. PyQt5.QtX11Extras or PySide2.QtX11Extras.
110109
f"{QtWidgets.__package__}.QtX11Extras")
111-
is_x11_build = True
110+
x11_build = True
112111
except ImportError:
113-
is_x11_build = False
112+
x11_build = False
114113
else:
115-
is_x11_build = hasattr(QtGui, "QX11Info")
116-
if is_x11_build:
117-
display = os.environ.get('DISPLAY')
118-
if display is None or not re.search(r':\d', display):
119-
raise RuntimeError('Invalid DISPLAY variable')
114+
x11_build = hasattr(QtGui, "QX11Info")
115+
if x11_build and matplotlib._c_internal_utils.display_is_invalid():
116+
raise RuntimeError('Invalid DISPLAY variable')
120117

121118
try:
122119
QtWidgets.QApplication.setAttribute(

lib/matplotlib/cbook/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ def _get_running_interactive_framework():
7373
if 'matplotlib.backends._macosx' in sys.modules:
7474
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
7575
return "macosx"
76-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
76+
if (sys.platform.startswith("linux")
77+
and _c_internal_utils.display_is_invalid()):
7778
return "headless"
7879
return None
7980

setupext.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,10 @@ def get_extensions(self):
349349
# c_internal_utils
350350
ext = Extension(
351351
"matplotlib._c_internal_utils", ["src/_c_internal_utils.c"],
352-
libraries=({"win32": ["ole32", "shell32", "user32"]}
353-
.get(sys.platform, [])))
352+
libraries=({
353+
"linux": ["dl"],
354+
"win32": ["ole32", "shell32", "user32"],
355+
}.get(sys.platform, [])))
354356
yield ext
355357
# contour
356358
ext = Extension(

src/_c_internal_utils.c

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
11
#define PY_SSIZE_T_CLEAN
22
#include <Python.h>
3+
#ifdef __linux__
4+
#include <dlfcn.h>
5+
#endif
36
#ifdef _WIN32
47
#include <Objbase.h>
58
#include <Shobjidl.h>
69
#include <Windows.h>
710
#endif
811

12+
static PyObject* mpl_display_is_invalid(PyObject* module)
13+
{
14+
#ifdef __linux__
15+
void* libX11;
16+
if ((libX11 = dlopen("libX11.so", RTLD_LAZY))) {
17+
struct Display* display = NULL;
18+
struct Display* (* XOpenDisplay)(char const*) =
19+
dlsym(libX11, "XOpenDisplay");
20+
int (* XCloseDisplay)(struct Display*) =
21+
dlsym(libX11, "XCloseDisplay");
22+
if (XOpenDisplay && XCloseDisplay
23+
&& (display = XOpenDisplay(NULL))) {
24+
XCloseDisplay(display);
25+
}
26+
if (dlclose(libX11)) {
27+
PyErr_SetString(PyExc_RuntimeError, dlerror());
28+
return NULL;
29+
}
30+
if (display) {
31+
Py_RETURN_FALSE;
32+
}
33+
}
34+
void* libwayland_client;
35+
if ((libwayland_client = dlopen("libwayland-client.so", RTLD_LAZY))) {
36+
struct wl_display* display = NULL;
37+
struct wl_display* (* wl_display_connect)(char const*) =
38+
dlsym(libwayland_client, "wl_display_connect");
39+
int (* wl_display_disconnect)(struct wl_display*) =
40+
dlsym(libwayland_client, "wl_display_disconnect");
41+
if (wl_display_connect && wl_display_connect
42+
&& (display = wl_display_connect(NULL))) {
43+
wl_display_disconnect(display);
44+
}
45+
if (dlclose(libwayland_client)) {
46+
PyErr_SetString(PyExc_RuntimeError, dlerror());
47+
return NULL;
48+
}
49+
if (display) {
50+
Py_RETURN_FALSE;
51+
}
52+
}
53+
Py_RETURN_TRUE;
54+
#else
55+
Py_RETURN_FALSE;
56+
#endif
57+
}
58+
959
static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module)
1060
{
1161
#ifdef _WIN32
@@ -66,6 +116,12 @@ static PyObject* mpl_SetForegroundWindow(PyObject* module, PyObject *arg)
66116
}
67117

68118
static PyMethodDef functions[] = {
119+
{"display_is_invalid", (PyCFunction)mpl_display_is_invalid, METH_NOARGS,
120+
"display_is_invalid()\n--\n\n"
121+
"Attempt to check whether the current X11 or Wayland display is invalid.\n\n"
122+
"Returns True if running on Linux and both XOpenDisplay(NULL) returns NULL\n"
123+
"(if libX11 can be loaded) and wl_display_connect(NULL) returns NULL\n"
124+
"(if libwayland-client can be loaded), False otherwise."},
69125
{"Win32_GetCurrentProcessExplicitAppUserModelID",
70126
(PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS,
71127
"Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n"
@@ -83,7 +139,7 @@ static PyMethodDef functions[] = {
83139
"always returns None."},
84140
{"Win32_SetForegroundWindow",
85141
(PyCFunction)mpl_SetForegroundWindow, METH_O,
86-
"Win32_SetForegroundWindow(hwnd)\n--\n\n"
142+
"Win32_SetForegroundWindow(hwnd, /)\n--\n\n"
87143
"Wrapper for Windows' SetForegroundWindow. On non-Windows platforms, \n"
88144
"a no-op."},
89145
{NULL, NULL}}; // sentinel.

0 commit comments

Comments
 (0)