Skip to content

Commit 2ad7160

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 28f41f9 commit 2ad7160

File tree

5 files changed

+71
-10
lines changed

5 files changed

+71
-10
lines changed

lib/matplotlib/backend_bases.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -2622,10 +2622,15 @@ def show(self):
26222622
warning in `.Figure.show`.
26232623
"""
26242624
# This should be overridden in GUI backends.
2625-
if cbook._get_running_interactive_framework() != "headless":
2626-
raise NonGuiException(
2627-
f"Matplotlib is currently using {get_backend()}, which is "
2628-
f"a non-GUI backend, so cannot show the figure.")
2625+
if sys.platform == "linux" and not os.environ.get("DISPLAY"):
2626+
# We cannot check _get_running_interactive_framework() ==
2627+
# "headless" because that would also suppress the warning when
2628+
# $DISPLAY exists but is invalid, which is more likely an error and
2629+
# thus warrants a warning.
2630+
return
2631+
raise NonGuiException(
2632+
f"Matplotlib is currently using {get_backend()}, which is a "
2633+
f"non-GUI backend, so cannot show the figure.")
26292634

26302635
def destroy(self):
26312636
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 (
@@ -115,10 +114,8 @@ def _create_qApp():
115114
is_x11_build = False
116115
else:
117116
is_x11_build = hasattr(QtGui, "QX11Info")
118-
if is_x11_build:
119-
display = os.environ.get('DISPLAY')
120-
if display is None or not re.search(r':\d', display):
121-
raise RuntimeError('Invalid DISPLAY variable')
117+
if is_x11_build and matplotlib._c_internal_utils.invalid_display():
118+
raise RuntimeError('Invalid DISPLAY variable')
122119

123120
try:
124121
QtWidgets.QApplication.setAttribute(

lib/matplotlib/cbook/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import numpy as np
2929

3030
import matplotlib
31+
import matplotlib as mpl
32+
import matplotlib._c_internal_utils
3133
from .deprecation import (
3234
deprecated, warn_deprecated,
3335
_rename_parameter, _delete_parameter, _make_keyword_only,
@@ -71,7 +73,8 @@ def _get_running_interactive_framework():
7173
if 'matplotlib.backends._macosx' in sys.modules:
7274
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
7375
return "macosx"
74-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
76+
if (sys.platform.startswith("linux")
77+
and mpl._c_internal_utils.invalid_display()):
7578
return "headless"
7679
return None
7780

setupext.py

+5
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,11 @@ def get_extensions(self):
422422
include_dirs=["extern"])
423423
add_numpy_flags(ext)
424424
yield ext
425+
# c_internal_utils
426+
ext = Extension(
427+
"matplotlib._c_internal_utils", ["src/_c_internal_utils.c"],
428+
libraries={"linux": ["dl"]}.get(sys.platform, []))
429+
yield ext
425430

426431

427432
class SampleData(OptionalPackage):

src/_c_internal_utils.c

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#define PY_SSIZE_T_CLEAN
2+
#include <Python.h>
3+
4+
#ifdef __linux__
5+
#include <dlfcn.h>
6+
#endif
7+
8+
static PyObject* mpl_invalid_display(PyObject* module)
9+
{
10+
#ifdef __linux__
11+
void* x11;
12+
typedef struct Display Display;
13+
Display* (* XOpenDisplay)(char* display_name);
14+
int (* XCloseDisplay)(Display* display);
15+
if (!(x11 = dlopen("libX11.so", RTLD_LAZY)) ||
16+
!(XOpenDisplay = dlsym(x11, "XOpenDisplay")) ||
17+
!(XCloseDisplay = dlsym(x11, "XCloseDisplay"))) {
18+
Py_RETURN_FALSE;
19+
}
20+
Display* display = XOpenDisplay(NULL);
21+
if (display) {
22+
XCloseDisplay(display);
23+
}
24+
if (dlclose(x11)) {
25+
PyErr_SetString(PyExc_RuntimeError, dlerror());
26+
return NULL;
27+
}
28+
if (display) {
29+
Py_RETURN_FALSE;
30+
} else {
31+
Py_RETURN_TRUE;
32+
}
33+
#else
34+
Py_RETURN_FALSE;
35+
#endif
36+
}
37+
38+
static PyMethodDef functions[] = {
39+
{"invalid_display", (PyCFunction)mpl_invalid_display, METH_NOARGS,
40+
"invalid_display()\n--\n\n"
41+
"Attempt to check whether the current X11 display is invalid.\n\n"
42+
"Returns True if running on Linux and libX11 can be loaded and \n"
43+
"XOpenDisplay(NULL) returns NULL, False otherwise."},
44+
{NULL, NULL}}; // sentinel.
45+
static PyModuleDef util_module = {
46+
PyModuleDef_HEAD_INIT, "_c_internal_utils", "", 0, functions, NULL, NULL, NULL, NULL};
47+
48+
PyMODINIT_FUNC PyInit__c_internal_utils(void)
49+
{
50+
return PyModule_Create(&util_module);
51+
}

0 commit comments

Comments
 (0)