Skip to content

test_grab_x11 failure when no X session is running #9577

Description

@kulikjak

What did you do?

I ran the pillow 12.2.0 test suite on a machine where XCB (X protocol) support is available, but without a running X session.

What did you expect to happen?

All tests should pass.

What actually happened?

test_grab_x11 failed with:

....
>           size, data = Image.core.grabscreen_x11(display_name)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E           OSError: X connection failed: error 5
....
>                   raise subprocess.CalledProcessError(retcode, args)
E                   subprocess.CalledProcessError: Command '['gnome-screenshot', '-f', '/tmp/tmpqqqjkvf6.png']' returned non-zero exit status 1.

../build/prototype/i386/usr/lib/python3.13/vendor-packages/PIL/ImageGrab.py:135: CalledProcessError
-------------------------------------------------------------------------------------- Captured stderr call ---------------------------------------------------------------------------------------

(gnome-screenshot:8318): Gtk-WARNING **: 16:20:16.856: cannot open display
....

What are your OS, Python and Pillow versions?

  • OS: Oracle Solaris
  • Python: 3.11 and 3.13
  • Pillow: 12.2.0
--------------------------------------------------------------------
Pillow 12.2.0
Python 3.13.12 (main, Mar 24 2026, 07:17:40) [GCC 15.2.0]
--------------------------------------------------------------------
Python executable is /bin/python3
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /builds/pillow/build/prototype/i386/usr/lib/python3.13/vendor-packages/PIL
Binary Pillow modules loaded from /builds/pillow/build/prototype/i386/usr/lib/python3.13/vendor-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 12.2.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.14.2
--- LITTLECMS2 support ok, loaded 2.17
--- WEBP support ok, loaded 1.6.0
*** AVIF support not installed
--- JPEG support ok, compiled for 9.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.3
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.2
--- LIBTIFF support ok, loaded 4.7.1
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------

Activity

  1. kulikjak commented on Apr 20, 2026

    @kulikjak
    ContributorAuthor

    Here is the full trace:

    =================================== FAILURES ===================================
    _________________________ TestImageGrab.test_grab_x11 __________________________
    
    bbox = None, include_layered_windows = False, all_screens = False
    xdisplay = None, window = None
    
        def grab(
            bbox: tuple[int, int, int, int] | None = None,
            include_layered_windows: bool = False,
            all_screens: bool = False,
            xdisplay: str | None = None,
            window: int | ImageWin.HWND | None = None,
        ) -> Image.Image:
            im: Image.Image
            if xdisplay is None:
                if sys.platform == "darwin":
                    fh, filepath = tempfile.mkstemp(".png")
                    os.close(fh)
                    args = ["screencapture"]
                    if window is not None:
                        args += ["-l", str(window)]
                    elif bbox:
                        left, top, right, bottom = bbox
                        args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
                    args += ["-x", filepath]
                    retcode = subprocess.call(args)
                    if retcode:
                        raise subprocess.CalledProcessError(retcode, args)
                    im = Image.open(filepath)
                    im.load()
                    os.unlink(filepath)
                    if bbox:
                        if window is not None:
                            # Determine if the window was in Retina mode or not
                            # by capturing it without the shadow,
                            # and checking how different the width is
                            fh, filepath = tempfile.mkstemp(".png")
                            os.close(fh)
                            args = ["screencapture", "-l", str(window), "-o", "-x", filepath]
                            retcode = subprocess.call(args)
                            if retcode:
                                raise subprocess.CalledProcessError(retcode, args)
                            with Image.open(filepath) as im_no_shadow:
                                retina = im.width - im_no_shadow.width > 100
                            os.unlink(filepath)
        
                            # Since screencapture's -R does not work with -l,
                            # crop the image manually
                            if retina:
                                left, top, right, bottom = bbox
                                im_cropped = im.resize(
                                    (right - left, bottom - top),
                                    box=tuple(coord * 2 for coord in bbox),
                                )
                            else:
                                im_cropped = im.crop(bbox)
                            im.close()
                            return im_cropped
                        else:
                            im_resized = im.resize((right - left, bottom - top))
                            im.close()
                            return im_resized
                    return im
                elif sys.platform == "win32":
                    if window is not None:
                        all_screens = -1
                    offset, size, data = Image.core.grabscreen_win32(
                        include_layered_windows,
                        all_screens,
                        int(window) if window is not None else 0,
                    )
                    im = Image.frombytes(
                        "RGB",
                        size,
                        data,
                        # RGB, 32-bit line padding, origin lower left corner
                        "raw",
                        "BGR",
                        (size[0] * 3 + 3) & -4,
                        -1,
                    )
                    if bbox:
                        x0, y0 = offset
                        left, top, right, bottom = bbox
                        im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
                    return im
            # Cast to Optional[str] needed for Windows and macOS.
            display_name: str | None = xdisplay
            try:
                if not Image.core.HAVE_XCB:
                    msg = "Pillow was built without XCB support"
                    raise OSError(msg)
    >           size, data = Image.core.grabscreen_x11(display_name)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    E           OSError: X connection failed: error 5
    
    ../build/prototype/i386/usr/lib/python3.13/vendor-packages/PIL/ImageGrab.py:119: OSError
    
    During handling of the above exception, another exception occurred:
    
    self = <Tests.test_imagegrab.TestImageGrab object at 0x7fe48d8c6e90>
    
        @skip_unless_feature("xcb")
        def test_grab_x11(self) -> None:
            try:
                if sys.platform not in ("win32", "darwin"):
    >               ImageGrab.grab()
    
    Tests/test_imagegrab.py:35: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
    
    bbox = None, include_layered_windows = False, all_screens = False
    xdisplay = None, window = None
    
        def grab(
            bbox: tuple[int, int, int, int] | None = None,
            include_layered_windows: bool = False,
            all_screens: bool = False,
            xdisplay: str | None = None,
            window: int | ImageWin.HWND | None = None,
        ) -> Image.Image:
            im: Image.Image
            if xdisplay is None:
                if sys.platform == "darwin":
                    fh, filepath = tempfile.mkstemp(".png")
                    os.close(fh)
                    args = ["screencapture"]
                    if window is not None:
                        args += ["-l", str(window)]
                    elif bbox:
                        left, top, right, bottom = bbox
                        args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
                    args += ["-x", filepath]
                    retcode = subprocess.call(args)
                    if retcode:
                        raise subprocess.CalledProcessError(retcode, args)
                    im = Image.open(filepath)
                    im.load()
                    os.unlink(filepath)
                    if bbox:
                        if window is not None:
                            # Determine if the window was in Retina mode or not
                            # by capturing it without the shadow,
                            # and checking how different the width is
                            fh, filepath = tempfile.mkstemp(".png")
                            os.close(fh)
                            args = ["screencapture", "-l", str(window), "-o", "-x", filepath]
                            retcode = subprocess.call(args)
                            if retcode:
                                raise subprocess.CalledProcessError(retcode, args)
                            with Image.open(filepath) as im_no_shadow:
                                retina = im.width - im_no_shadow.width > 100
                            os.unlink(filepath)
        
                            # Since screencapture's -R does not work with -l,
                            # crop the image manually
                            if retina:
                                left, top, right, bottom = bbox
                                im_cropped = im.resize(
                                    (right - left, bottom - top),
                                    box=tuple(coord * 2 for coord in bbox),
                                )
                            else:
                                im_cropped = im.crop(bbox)
                            im.close()
                            return im_cropped
                        else:
                            im_resized = im.resize((right - left, bottom - top))
                            im.close()
                            return im_resized
                    return im
                elif sys.platform == "win32":
                    if window is not None:
                        all_screens = -1
                    offset, size, data = Image.core.grabscreen_win32(
                        include_layered_windows,
                        all_screens,
                        int(window) if window is not None else 0,
                    )
                    im = Image.frombytes(
                        "RGB",
                        size,
                        data,
                        # RGB, 32-bit line padding, origin lower left corner
                        "raw",
                        "BGR",
                        (size[0] * 3 + 3) & -4,
                        -1,
                    )
                    if bbox:
                        x0, y0 = offset
                        left, top, right, bottom = bbox
                        im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
                    return im
            # Cast to Optional[str] needed for Windows and macOS.
            display_name: str | None = xdisplay
            try:
                if not Image.core.HAVE_XCB:
                    msg = "Pillow was built without XCB support"
                    raise OSError(msg)
                size, data = Image.core.grabscreen_x11(display_name)
            except OSError:
                if display_name is None and sys.platform not in ("darwin", "win32"):
                    if shutil.which("gnome-screenshot"):
                        args = ["gnome-screenshot", "-f"]
                    elif shutil.which("grim"):
                        args = ["grim"]
                    elif shutil.which("spectacle"):
                        args = ["spectacle", "-n", "-b", "-f", "-o"]
                    else:
                        raise
                    fh, filepath = tempfile.mkstemp(".png")
                    os.close(fh)
                    args.append(filepath)
                    retcode = subprocess.call(args)
                    if retcode:
    >                   raise subprocess.CalledProcessError(retcode, args)
    E                   subprocess.CalledProcessError: Command '['gnome-screenshot', '-f', '/tmp/tmpjeopzhzn.png']' returned non-zero exit status 1.
    
    ../build/prototype/i386/usr/lib/python3.13/vendor-packages/PIL/ImageGrab.py:135: CalledProcessError
    ----------------------------- Captured stderr call -----------------------------
    
    (gnome-screenshot:15211): Gtk-WARNING **: 16:25:32.468: cannot open display: 
    =========================== short test summary info ============================
    SKIPPED [1] Tests/test_imagegrab.py:16: requires Windows or macOS
    SKIPPED [1] Tests/test_imagegrab.py:41: tests missing XCB
    SKIPPED [1] Tests/test_imagegrab.py:63: Only runs on macOS CI
    SKIPPED [1] Tests/test_imagegrab.py:87: macOS and Windows only
    SKIPPED [1] Tests/test_imagegrab.py:127: Windows only
    SKIPPED [1] Tests/test_imagegrab.py:139: Windows only
    SKIPPED [3] Tests/test_imagegrab.py:155: Linux with wl-clipboard only
    SKIPPED [2] Tests/test_imagegrab.py:171: Linux with wl-clipboard only
    FAILED Tests/test_imagegrab.py::TestImageGrab::test_grab_x11 - subprocess.Cal...
    =================== 1 failed, 2 passed, 11 skipped in 3.00s ====================
    gmake: *** [/builds/jkulik/pillow-12.2.0/make-rules/python-common.mk:151: /builds/pillow/build/amd64-3.13/.tested] Error 1
    
  2. radarhere commented on Apr 20, 2026

    @radarhere
    Member

    We're catching the 'X connection failed' OSError, and then falling back to trying gnome-screenshot instead. #9321 started raising subprocess.CalledProcessError when gnome-screenshot fails. So from one perspective, the real problem is that gnome-screenshot isn't working.

    I've created #9578 to catch the error.

  3. kulikjak commented on Apr 21, 2026

    @kulikjak
    ContributorAuthor

    Thanks! With #9578, the test suite is green again and reports:

    SKIPPED [1] Tests/test_imagegrab.py:39: Command '['gnome-screenshot', '-f', '/tmp/tmp2zm4fuy7.png']' returned non-zero exit status 1.
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions