Skip to content

ponty/PyVirtualDisplay

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
ponty
Nov 4, 2023
1d03358 · Nov 4, 2023
Nov 4, 2023
Jun 11, 2022
Sep 18, 2023
Nov 4, 2023
Apr 8, 2014
Oct 14, 2020
Dec 28, 2021
Feb 11, 2011
May 10, 2020
Nov 4, 2023
Jun 12, 2022
Nov 4, 2023
Jan 18, 2022
Jan 18, 2022
Jun 28, 2020
Jan 23, 2022
Jan 23, 2022
Nov 4, 2023
Nov 4, 2023

Repository files navigation

pyvirtualdisplay is a python wrapper for Xvfb, Xephyr and Xvnc programs. They all use the X Window System (not Windows, not macOS) The selected program should be installed first so that it can be started without a path, otherwise pyvirtualdisplay will not find the program.

Links:

workflow

Features:

  • python wrapper
  • supported python versions: 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12
  • back-ends: Xvfb, Xephyr and Xvnc

Possible applications:

  • headless run
  • GUI testing
  • automatic GUI screenshot

Installation

install the program:

$ python3 -m pip install pyvirtualdisplay

optional: Pillow should be installed for smartdisplay submodule:

$ python3 -m pip install pillow

optional: EasyProcess should be installed for some examples:

$ python3 -m pip install EasyProcess

optional: xmessage and gnumeric should be installed for some examples.

On Ubuntu 22.04:

$ sudo apt install x11-utils gnumeric

If you get this error message on Linux then your Pillow version is old.

ImportError: ImageGrab is macOS and Windows only

Install all dependencies and backends on Ubuntu 22.04:

$ sudo apt-get install xvfb xserver-xephyr tigervnc-standalone-server x11-utils gnumeric
$ python3 -m pip install pyvirtualdisplay pillow EasyProcess

Usage

Controlling the display with context manager:

from pyvirtualdisplay import Display
with Display() as disp:
    # display is active
    print(disp.is_alive()) # True
# display is stopped
print(disp.is_alive()) # False

Controlling the display with start() and stop() methods (not recommended):

from pyvirtualdisplay import Display
disp = Display()
disp.start()
# display is active
disp.stop()
# display is stopped

After Xvfb display is activated "DISPLAY" environment variable is set for Xvfb. (e.g. os.environ["DISPLAY"] = :1) After Xvfb display is stopped start() and stop() are not allowed to be called again, "DISPLAY" environment variable is restored to its original value.

Selecting Xvfb backend:

disp=Display()
# or
disp=Display(visible=False)
# or
disp=Display(backend="xvfb")

Selecting Xephyr backend:

disp=Display(visible=True)
# or
disp=Display(backend="xephyr")

Selecting Xvnc backend:

disp=Display(backend="xvnc")

Setting display size:

disp=Display(size=(100, 60))

Setting display color depth:

disp=Display(color_depth=24)

Headless run

A messagebox is displayed on a hidden display.

# pyvirtualdisplay/examples/headless.py

"Start Xvfb server. Open xmessage window."

from easyprocess import EasyProcess

from pyvirtualdisplay import Display

with Display(visible=False, size=(100, 60)) as disp:
    with EasyProcess(["xmessage", "hello"]) as proc:
        proc.wait()

Run it:

$ python3 -m pyvirtualdisplay.examples.headless

If visible=True then a nested Xephyr window opens and the GUI can be controlled.

vncserver

The same as headless example, but it can be controlled with a VNC client.

# pyvirtualdisplay/examples/vncserver.py

"Start virtual VNC server. Connect with: vncviewer localhost:5904"

from easyprocess import EasyProcess

from pyvirtualdisplay import Display

with Display(backend="xvnc", size=(100, 60), rfbport=5904) as disp:
    with EasyProcess(["xmessage", "hello"]) as proc:
        proc.wait()

Run it:

$ python3 -m pyvirtualdisplay.examples.vncserver

Check it with vncviewer:

$ vncviewer localhost:5904

GUI Test

# pyvirtualdisplay/examples/lowres.py

"Testing gnumeric on low resolution."
from easyprocess import EasyProcess

from pyvirtualdisplay import Display

# start Xephyr
with Display(visible=True, size=(320, 240)) as disp:
    # start Gnumeric
    with EasyProcess(["gnumeric"]) as proc:
        proc.wait()

Run it:

$ python3 -m pyvirtualdisplay.examples.lowres

Image:

Screenshot

# pyvirtualdisplay/examples/screenshot.py

"Create screenshot of xmessage in background using 'smartdisplay' submodule"
from easyprocess import EasyProcess

from pyvirtualdisplay.smartdisplay import SmartDisplay

# 'SmartDisplay' instead of 'Display'
# It has 'waitgrab()' method.
# It has more dependencies than Display.
with SmartDisplay() as disp:
    with EasyProcess(["xmessage", "hello"]):
        # wait until something is displayed on the virtual display (polling method)
        # and then take a fullscreen screenshot
        # and then crop it. Background is black.
        img = disp.waitgrab()
img.save("xmessage.png")

Run it:

$ python3 -m pyvirtualdisplay.examples.screenshot

Image:

Nested Xephyr

# pyvirtualdisplay/examples/nested.py

"Nested Xephyr servers"
from easyprocess import EasyProcess

from pyvirtualdisplay import Display

with Display(visible=True, size=(220, 180), bgcolor="black"):
    with Display(visible=True, size=(200, 160), bgcolor="white"):
        with Display(visible=True, size=(180, 140), bgcolor="black"):
            with Display(visible=True, size=(160, 120), bgcolor="white"):
                with Display(visible=True, size=(140, 100), bgcolor="black"):
                    with Display(visible=True, size=(120, 80), bgcolor="white"):
                        with Display(visible=True, size=(100, 60), bgcolor="black"):
                            with EasyProcess(["xmessage", "hello"]) as proc:
                                proc.wait()

Run it:

$ python3 -m pyvirtualdisplay.examples.nested

Image:

xauth

Some programs require a functional Xauthority file. PyVirtualDisplay can generate one and set the appropriate environment variables if you pass use_xauth=True to the Display constructor. Note however that this feature needs xauth installed, otherwise a pyvirtualdisplay.xauth.NotFoundError is raised.

Mouse cursor

The cursor can be disabled in Xvfb using an extra argument which is passed directly to Xvfb:

with Display(backend="xvfb", extra_args=["-nocursor"]):
    ...

Based on Xvfb help:

...
-nocursor              disable the cursor
...

Concurrency

If more X servers are started at the same time then there is race for free display numbers.

"Recent X servers as of version 1.13 (Xvfb, too) support the -displayfd command line option: It will make the X server choose the display itself" https://stackoverflow.com/questions/2520704/find-a-free-x11-display-number/

Version 1.13 was released in 2012: https://www.x.org/releases/individual/xserver/

First help text is checked (e.g. Xvfb -help) to find if -displayfd flag is available. If -displayfd flag is available then it is used to choose the display number. If not then a free display number is generated and there are 10 retries by default which should be enough for starting 10 X servers at the same time.

displayfd usage is disabled on macOS because it doesn't work with XQuartz-2.7.11, always 0 is returned.

Thread safety

All previous examples are not thread-safe, because pyvirtualdisplay replaces $DISPLAY environment variable in global os.environ in start() and sets back to original value in stop(). To make it thread-safe you have to manage the $DISPLAY variable. Set manage_global_env to False in constructor.

# pyvirtualdisplay/examples/threadsafe.py

"Start Xvfb server and open xmessage window. Thread safe."

import threading

from easyprocess import EasyProcess

from pyvirtualdisplay.smartdisplay import SmartDisplay


def thread_function(index):
    # manage_global_env=False is thread safe
    with SmartDisplay(manage_global_env=False) as disp:
        cmd = ["xmessage", str(index)]
        # disp.new_display_var should be used for new processes
        # disp.env() copies global os.environ and adds disp.new_display_var
        with EasyProcess(cmd, env=disp.env()):
            img = disp.waitgrab()
            img.save("xmessage{}.png".format(index))


t1 = threading.Thread(target=thread_function, args=(1,))
t2 = threading.Thread(target=thread_function, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()

Run it:

$ python3 -m pyvirtualdisplay.examples.threadsafe

Images:

Hierarchy

Alt text