Skip to content

Commit 653f3d0

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. Note that we were *already* relying on $DISPLAY being correctly set before, and this PR also doesn't return "invalid display" if we can't load X11, so this should not make anything worse on Wayland (at worst we'll just fail to detect headlessness like before).
1 parent 3d725f6 commit 653f3d0

File tree

5 files changed

+55
-12
lines changed

5 files changed

+55
-12
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

+2-5
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 (
@@ -113,10 +112,8 @@ def _create_qApp():
113112
is_x11_build = False
114113
else:
115114
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')
115+
if is_x11_build and matplotlib._c_internal_utils.invalid_display():
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.invalid_display()):
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

+38
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
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_invalid_display(PyObject* module)
13+
{
14+
#ifdef __linux__
15+
void* libX11;
16+
typedef struct Display Display;
17+
Display* (* XOpenDisplay)(char* display_name);
18+
int (* XCloseDisplay)(Display* display);
19+
if (!(libX11 = dlopen("libX11.so", RTLD_LAZY)) ||
20+
!(XOpenDisplay = dlsym(libX11, "XOpenDisplay")) ||
21+
!(XCloseDisplay = dlsym(libX11, "XCloseDisplay"))) {
22+
Py_RETURN_FALSE;
23+
}
24+
Display* display = XOpenDisplay(NULL);
25+
if (display) {
26+
XCloseDisplay(display);
27+
}
28+
if (dlclose(libX11)) {
29+
PyErr_SetString(PyExc_RuntimeError, dlerror());
30+
return NULL;
31+
}
32+
if (display) {
33+
Py_RETURN_FALSE;
34+
} else {
35+
Py_RETURN_TRUE;
36+
}
37+
#else
38+
Py_RETURN_FALSE;
39+
#endif
40+
}
41+
942
static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module)
1043
{
1144
#ifdef _WIN32
@@ -66,6 +99,11 @@ static PyObject* mpl_SetForegroundWindow(PyObject* module, PyObject *arg)
6699
}
67100

68101
static PyMethodDef functions[] = {
102+
{"invalid_display", (PyCFunction)mpl_invalid_display, METH_NOARGS,
103+
"invalid_display()\n--\n\n"
104+
"Attempt to check whether the current X11 display is invalid.\n\n"
105+
"Returns True if running on Linux and libX11 can be loaded and \n"
106+
"XOpenDisplay(NULL) returns NULL, False otherwise."},
69107
{"Win32_GetCurrentProcessExplicitAppUserModelID",
70108
(PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS,
71109
"Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n"

0 commit comments

Comments
 (0)