diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..2d2b40f
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,70 @@
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python package
+
+on:
+  schedule:
+    # * is a special character in YAML so you have to quote this string
+    - cron:  '30 5 1 * *'
+  push:
+  pull_request:
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: 
+          - "ubuntu-18.04"
+          - "macos-10.15"
+          - "macos-11"
+        python-version: 
+          - "3.9"
+          - "3.10"
+        include:
+          - python-version: "3.6"
+            os: ubuntu-20.04
+          - python-version: "3.7"
+            os: ubuntu-20.04
+          - python-version: "3.8"
+            os: ubuntu-20.04
+          - python-version: "3.9"
+            os: ubuntu-20.04
+          - python-version: "3.10"
+            os: ubuntu-20.04
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install Linux dependencies
+      if: startsWith(matrix.os, 'ubuntu')
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y tigervnc-standalone-server xserver-xephyr gnumeric x11-utils
+    - name: Install MacOS dependencies
+      if: startsWith(matrix.os, 'macos')
+      run: |
+        brew install --cask xquartz
+        # https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#adding-a-system-path
+        echo "/opt/X11/bin" >> $GITHUB_PATH  
+        # https://github.com/ponty/PyVirtualDisplay/issues/42
+        mkdir /tmp/.X11-unix
+        sudo chmod 1777 /tmp/.X11-unix
+        sudo chown root /tmp/.X11-unix
+    - name: Xvfb -help
+      run: |
+        Xvfb -help
+    - name: pip install
+      run: |
+        python -m pip install .
+        pip install -r requirements-test.txt
+    - name: Test with pytest
+      run: |
+        cd tests
+        pytest -v .
+    - name: Lint
+      if: matrix.os == 'ubuntu-20.04'
+      run: |
+        ./lint.sh
diff --git a/.travis.yml b/.travis.yml.bak
similarity index 100%
rename from .travis.yml
rename to .travis.yml.bak
diff --git a/README.md b/README.md
index 11bf130..3f012b8 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,11 @@ Links:
  * home: https://github.com/ponty/pyvirtualdisplay
  * PYPI: https://pypi.python.org/pypi/pyvirtualdisplay
 
-[![Build Status](https://travis-ci.org/ponty/pyvirtualdisplay.svg?branch=master)](https://travis-ci.org/ponty/pyvirtualdisplay)
+![workflow](https://github.com/ponty/pyvirtualdisplay/actions/workflows/main.yml/badge.svg)
 
 Features:
  - python wrapper
- - supported python versions: 3.6, 3.7, 3.8, 3.9
+ - supported python versions: 3.6, 3.7, 3.8, 3.9, 3.10
  - back-ends:  [Xvfb][1], [Xephyr][2] and [Xvnc][3]
 
 Possible applications:
@@ -31,16 +31,28 @@ optional: [Pillow][pillow] should be installed for ``smartdisplay`` submodule:
 $ python3 -m pip install pillow
 ```
 
+optional: [EasyProcess][EasyProcess] should be installed for some examples:
+
+```console
+$ python3 -m pip install EasyProcess
+```
+optional: xmessage and gnumeric should be installed for some examples.
+
+On Ubuntu 20.04:
+```console
+$ 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
 ```
 
-on Ubuntu 20.04:
+Install all dependencies and backends on Ubuntu 20.04:
 
 ```console
-$ sudo apt-get install xvfb xserver-xephyr tigervnc-standalone-server xfonts-base
-$ python3 -m pip install pyvirtualdisplay pillow
+$ sudo apt-get install xvfb xserver-xephyr tigervnc-standalone-server x11-utils gnumeric
+$ python3 -m pip install pyvirtualdisplay pillow EasyProcess
 ```
 
 Usage
@@ -59,7 +71,7 @@ disp.stop()
 
 After Xvfb display is activated "DISPLAY" environment variable is set for Xvfb.
 (e.g. `os.environ["DISPLAY"] = :1`)
-After Xvfb display is stopped "DISPLAY" environment variable is restored to its original value.
+After Xvfb display is stopped `start()` and `stop()` are not allowed to be called again, "DISPLAY" environment variable is restored to its original value. 
 
 
 Controlling the display with context manager:
@@ -109,7 +121,7 @@ disp=Display(color_depth=24)
 Headless run
 ------------
 
-The display is hidden.
+A messagebox is displayed on a hidden display. 
 
 ```py
 # pyvirtualdisplay/examples/headless.py
@@ -125,6 +137,10 @@ with Display(visible=False, size=(100, 60)) as disp:
         proc.wait()
 
 ```
+Run it:
+```console
+$ python3 -m pyvirtualdisplay.examples.headless
+```
 
 If `visible=True` then a nested Xephyr window opens and the GUI can be controlled.
 
@@ -296,34 +312,46 @@ Set `manage_global_env` to `False` in constructor.
 ```py
 # pyvirtualdisplay/examples/threadsafe.py
 
-"Start Xvfb server. Open xmessage window. Thread safe."
+"Start Xvfb server and open xmessage window. Thread safe."
+
+import threading
 
 from easyprocess import EasyProcess
 
-from pyvirtualdisplay import Display
+from pyvirtualdisplay.smartdisplay import SmartDisplay
 
-# manage_global_env=False is thread safe
-with Display(manage_global_env=False) as disp:
-    # disp.new_display_var should be used for new processes
-    print("disp.new_display_var=" + disp.new_display_var)
 
-    # disp.env() copies global os.environ and adds disp.new_display_var
-    print("disp.env()['DISPLAY']=" + disp.env()["DISPLAY"])
+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))
 
-    # set $DISPLAY for subprocesses
-    with EasyProcess(["xmessage", "-timeout", "1", "hello"], env=disp.env()) as proc:
-        proc.wait()
+
+t1 = threading.Thread(target=thread_function, args=(1,))
+t2 = threading.Thread(target=thread_function, args=(2,))
+t1.start()
+t2.start()
+t1.join()
+t2.join()
 
 ```
 
-<!-- embedme doc/gen/python3_-m_pyvirtualdisplay.examples.threadsafe.txt -->
+
 Run it:
 ```console
 $ python3 -m pyvirtualdisplay.examples.threadsafe
-disp.new_display_var=:2
-disp.env()['DISPLAY']=:2
 ```
 
+Images:
+
+![](doc/gen/xmessage1.png)
+![](doc/gen/xmessage2.png)
+
 
 Hierarchy
 =========
@@ -335,3 +363,4 @@ Hierarchy
 [3]: https://tigervnc.org/
 [pillow]: https://pillow.readthedocs.io
 [environ]: https://docs.python.org/3/library/os.html#os.environ
+[EasyProcess]: https://github.com/ponty/EasyProcess
\ No newline at end of file
diff --git a/Vagrantfile b/Vagrantfile
index 6f1d39b..ed3eb57 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -1,115 +1,18 @@
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-
-# All Vagrant configuration is done below. The "2" in Vagrant.configure
-# configures the configuration version (we support older styles for
-# backwards compatibility). Please don't change it unless you know what
-# you're doing.
 Vagrant.configure(2) do |config|
-  # The most common configuration options are documented and commented below.
-  # For a complete reference, please see the online documentation at
-  # https://docs.vagrantup.com.
-
-  # Every Vagrant development environment requires a box. You can search for
-  # boxes at https://atlas.hashicorp.com/search.
   config.vm.box = "ubuntu/focal64"
 
-  # Disable automatic box update checking. If you disable this, then
-  # boxes will only be checked for updates when the user runs
-  # `vagrant box outdated`. This is not recommended.
-  # config.vm.box_check_update = false
-
-  # Create a forwarded port mapping which allows access to a specific port
-  # within the machine from a port on the host machine. In the example below,
-  # accessing "localhost:8080" will access port 80 on the guest machine.
-  # config.vm.network "forwarded_port", guest: 80, host: 8080
-
-  # Create a private network, which allows host-only access to the machine
-  # using a specific IP.
-  # config.vm.network "private_network", ip: "192.168.33.10"
-
-  # Create a public network, which generally matched to bridged network.
-  # Bridged networks make the machine appear as another physical device on
-  # your network.
-  # config.vm.network "public_network"
-
-  # Share an additional folder to the guest VM. The first argument is
-  # the path on the host to the actual folder. The second argument is
-  # the path on the guest to mount the folder. And the optional third
-  # argument is a set of non-required options.
-  # config.vm.synced_folder "../data", "/vagrant_data"
-
-  # Provider-specific configuration so you can fine-tune various
-  # backing providers for Vagrant. These expose provider-specific options.
-  # Example for VirtualBox:
-  #
-   config.vm.provider "virtualbox" do |vb|
-    # Display the VirtualBox GUI when booting the machine
+  config.vm.provider "virtualbox" do |vb|
     #vb.gui = true
- 
-    # Customize the amount of memory on the VM:
     vb.memory = "2048"
 
-    vb.name = "pyvirtualdisplay_2004"
+    vb.name = "pyvirtualdisplay_ubuntu2004"
 
     # 	https://bugs.launchpad.net/cloud-images/+bug/1829625
-    vb.customize ["modifyvm", :id, "--uart1", "0x3F8", "4"]
-    vb.customize ["modifyvm", :id, "--uartmode1", "file", "./ttyS0.log"]
+    # vb.customize ["modifyvm", :id, "--uart1", "0x3F8", "4"]
+    # vb.customize ["modifyvm", :id, "--uartmode1", "file", "./ttyS0.log"]
   end
-  #
-  # View the documentation for the provider you are using for more
-  # information on available options.
-
-  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
-  # such as FTP and Heroku are also available. See the documentation at
-  # https://docs.vagrantup.com/v2/push/atlas.html for more information.
-  # config.push.define "atlas" do |push|
-  #   push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
-  # end
-
-  # Enable provisioning with a shell script. Additional provisioners such as
-  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
-  # documentation for more information about their specific syntax and use.
-  $script = "
-  export DEBIAN_FRONTEND=noninteractive
-  echo 'export export LC_ALL=C' >> /home/vagrant/.profile
-  
-# install python versions
-  sudo add-apt-repository --yes  ppa:deadsnakes/ppa
-  sudo apt-get update
-  sudo apt-get install -y python3.6-dev
-  sudo apt-get install -y python3.7-dev
-  sudo apt-get install -y python3.8-dev
-  sudo apt-get install -y python3-distutils
-  sudo apt-get install -y python3.9-dev
-  sudo apt-get install -y python3.9-distutils
 
-# tools
-  sudo apt-get install -y mc python3-pip xvfb
+  config.vm.provision "shell", path: "tests/vagrant/ubuntu2004.sh"
 
-# for pillow source install
-#  sudo apt-get install -y libjpeg-dev zlib1g-dev
-
-# project dependencies
-  sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
-  
-# test dependencies
-  sudo apt-get install -y gnumeric 
-  sudo apt-get install -y x11-utils #   for: xmessage
-  sudo apt-get install -y x11-apps  #   for: xlogo
-  sudo pip3 install tox
-  
-# doc dependencies
-  sudo apt-get install -y npm xtightvncviewer
-  sudo npm install -g npx
-#  sudo pip install -r /vagrant/requirements-doc.txt
-  
-  "
-      config.vm.provision "shell", inline: $script
-
-      config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]       
-          
-       
+  config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]
 end
-     
-
diff --git a/Vagrantfile.14.04.rb b/Vagrantfile.14.04.rb
deleted file mode 100644
index a656f4a..0000000
--- a/Vagrantfile.14.04.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-
-# All Vagrant configuration is done below. The "2" in Vagrant.configure
-# configures the configuration version (we support older styles for
-# backwards compatibility). Please don't change it unless you know what
-# you're doing.
-Vagrant.configure(2) do |config|
-  # The most common configuration options are documented and commented below.
-  # For a complete reference, please see the online documentation at
-  # https://docs.vagrantup.com.
-
-  # Every Vagrant development environment requires a box. You can search for
-  # boxes at https://atlas.hashicorp.com/search.
-  config.vm.box = "ubuntu/trusty64"
-
-  # Disable automatic box update checking. If you disable this, then
-  # boxes will only be checked for updates when the user runs
-  # `vagrant box outdated`. This is not recommended.
-  # config.vm.box_check_update = false
-
-  # Create a forwarded port mapping which allows access to a specific port
-  # within the machine from a port on the host machine. In the example below,
-  # accessing "localhost:8080" will access port 80 on the guest machine.
-  # config.vm.network "forwarded_port", guest: 80, host: 8080
-
-  # Create a private network, which allows host-only access to the machine
-  # using a specific IP.
-  # config.vm.network "private_network", ip: "192.168.33.10"
-
-  # Create a public network, which generally matched to bridged network.
-  # Bridged networks make the machine appear as another physical device on
-  # your network.
-  # config.vm.network "public_network"
-
-  # Share an additional folder to the guest VM. The first argument is
-  # the path on the host to the actual folder. The second argument is
-  # the path on the guest to mount the folder. And the optional third
-  # argument is a set of non-required options.
-  # config.vm.synced_folder "../data", "/vagrant_data"
-
-  # Provider-specific configuration so you can fine-tune various
-  # backing providers for Vagrant. These expose provider-specific options.
-  # Example for VirtualBox:
-  #
-   config.vm.provider "virtualbox" do |vb|
-    vb.name = "pyvirtualdisplay_1404"
-    #   # Display the VirtualBox GUI when booting the machine
-  #   vb.gui = true
-  #
-     # Customize the amount of memory on the VM:
-     vb.memory = "2048"
-   end
-  #
-  # View the documentation for the provider you are using for more
-  # information on available options.
-
-  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
-  # such as FTP and Heroku are also available. See the documentation at
-  # https://docs.vagrantup.com/v2/push/atlas.html for more information.
-  # config.push.define "atlas" do |push|
-  #   push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
-  # end
-
-  # Enable provisioning with a shell script. Additional provisioners such as
-  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
-  # documentation for more information about their specific syntax and use.
-  $script = "
-  export DEBIAN_FRONTEND=noninteractive
-  echo 'export export LC_ALL=C' >> /home/vagrant/.profile
-  
-# install python versions
-  sudo add-apt-repository --yes  ppa:deadsnakes/ppa
-  sudo apt-get update
-  sudo apt-get install -y python3.6-dev
-  sudo apt-get install -y python3.7-dev
-  # sudo apt-get install -y python3.8-dev
-  # sudo apt-get install -y python3-distutils
-
-# tools
-  sudo apt-get install -y mc xvfb curl
-
-# for pillow source install
-#  sudo apt-get install -y libjpeg-dev zlib1g-dev
-
-# project dependencies
-  sudo apt-get install -y xvfb xserver-xephyr vnc4server
-  
-# test dependencies
-  sudo apt-get install -y gnumeric 
-  sudo apt-get install -y x11-utils #   for: xmessage
-  sudo apt-get install -y x11-apps  #   for: xlogo
-  sudo curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
-  sudo python3.6 /tmp/get-pip.py
-  sudo python3.6 -m pip install tox
-  
-# doc dependencies
-#  sudo apt-get install -y imagemagick graphviz
-#  sudo pip install -r /vagrant/requirements-doc.txt
-  
-  "
-      config.vm.provision "shell", inline: $script
-          
-      config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]       
-       
-end
-     
-
-# export VAGRANT_VAGRANTFILE=Vagrantfile.14.04.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
-# vagrant up && vagrant ssh
diff --git a/Vagrantfile.18.04.rb b/Vagrantfile.18.04.rb
deleted file mode 100644
index ceee3b4..0000000
--- a/Vagrantfile.18.04.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-
-# All Vagrant configuration is done below. The "2" in Vagrant.configure
-# configures the configuration version (we support older styles for
-# backwards compatibility). Please don't change it unless you know what
-# you're doing.
-Vagrant.configure(2) do |config|
-  # The most common configuration options are documented and commented below.
-  # For a complete reference, please see the online documentation at
-  # https://docs.vagrantup.com.
-
-  # Every Vagrant development environment requires a box. You can search for
-  # boxes at https://atlas.hashicorp.com/search.
-  config.vm.box = "ubuntu/bionic64"
-
-  # Disable automatic box update checking. If you disable this, then
-  # boxes will only be checked for updates when the user runs
-  # `vagrant box outdated`. This is not recommended.
-  # config.vm.box_check_update = false
-
-  # Create a forwarded port mapping which allows access to a specific port
-  # within the machine from a port on the host machine. In the example below,
-  # accessing "localhost:8080" will access port 80 on the guest machine.
-  # config.vm.network "forwarded_port", guest: 80, host: 8080
-
-  # Create a private network, which allows host-only access to the machine
-  # using a specific IP.
-  # config.vm.network "private_network", ip: "192.168.33.10"
-
-  # Create a public network, which generally matched to bridged network.
-  # Bridged networks make the machine appear as another physical device on
-  # your network.
-  # config.vm.network "public_network"
-
-  # Share an additional folder to the guest VM. The first argument is
-  # the path on the host to the actual folder. The second argument is
-  # the path on the guest to mount the folder. And the optional third
-  # argument is a set of non-required options.
-  # config.vm.synced_folder "../data", "/vagrant_data"
-
-  # Provider-specific configuration so you can fine-tune various
-  # backing providers for Vagrant. These expose provider-specific options.
-  # Example for VirtualBox:
-  #
-   config.vm.provider "virtualbox" do |vb|
-    vb.name = "pyvirtualdisplay_1804"
-  #   # Display the VirtualBox GUI when booting the machine
-  #   vb.gui = true
-  #
-     # Customize the amount of memory on the VM:
-    vb.memory = "4096" # ste high because of Xephyr memory leak
-   end
-  #
-  # View the documentation for the provider you are using for more
-  # information on available options.
-
-  # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
-  # such as FTP and Heroku are also available. See the documentation at
-  # https://docs.vagrantup.com/v2/push/atlas.html for more information.
-  # config.push.define "atlas" do |push|
-  #   push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
-  # end
-
-  # Enable provisioning with a shell script. Additional provisioners such as
-  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
-  # documentation for more information about their specific syntax and use.
-  $script = "
-  export DEBIAN_FRONTEND=noninteractive
-  echo 'export export LC_ALL=C' >> /home/vagrant/.profile
-  
-# install python versions
-  sudo add-apt-repository --yes  ppa:deadsnakes/ppa
-  sudo apt-get update
-  sudo apt-get install -y python3.6-dev
-  sudo apt-get install -y python3.7-dev
-  sudo apt-get install -y python3.8-dev
-  sudo apt-get install -y python3-distutils
-  sudo apt-get install -y python3.9-dev
-  sudo apt-get install -y python3.9-distutils
-
-# tools
-  sudo apt-get install -y mc python3-pip xvfb
-
-# for pillow source install
-#  sudo apt-get install -y libjpeg-dev zlib1g-dev
-
-# project dependencies
-  sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
-  
-# test dependencies
-  sudo apt-get install -y gnumeric 
-  sudo apt-get install -y x11-utils #   for: xmessage
-  sudo apt-get install -y x11-apps  #   for: xlogo
-  sudo pip3 install tox
-  
-# doc dependencies
-  sudo apt-get install -y npm xtightvncviewer
-  sudo npm install -g npx
-#  sudo pip install -r /vagrant/requirements-doc.txt
-  
-  "
-      config.vm.provision "shell", inline: $script
-
-      config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]       
-          
-       
-end
-     
-
-# export VAGRANT_VAGRANTFILE=Vagrantfile.18.04.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
-# vagrant up && vagrant ssh
diff --git a/debian/changelog b/debian/changelog
index 494cdbc..24b9479 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pyvirtualdisplay (3.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 15 May 2022 12:57:06 -0000
+
 pyvirtualdisplay (2.2-1) unstable; urgency=medium
 
   [ Ondřej Nový ]
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png b/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png
index 4c4afa2..2f5a53e 100644
Binary files a/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png and b/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png differ
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.txt b/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.txt
index 04416a2..963329f 100644
--- a/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.txt
+++ b/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.txt
@@ -1 +1 @@
-$ python3 -m pyvirtualdisplay.examples.lowres
\ No newline at end of file
+$ python3 -m pyvirtualdisplay.examples.lowres
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.txt b/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.txt
index 4905f08..0fbbbc7 100644
--- a/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.txt
+++ b/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.txt
@@ -1 +1 @@
-$ python3 -m pyvirtualdisplay.examples.nested
\ No newline at end of file
+$ python3 -m pyvirtualdisplay.examples.nested
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.screenshot.txt b/doc/gen/python3_-m_pyvirtualdisplay.examples.screenshot.txt
index 7a87779..9183182 100644
--- a/doc/gen/python3_-m_pyvirtualdisplay.examples.screenshot.txt
+++ b/doc/gen/python3_-m_pyvirtualdisplay.examples.screenshot.txt
@@ -1 +1 @@
-$ python3 -m pyvirtualdisplay.examples.screenshot
\ No newline at end of file
+$ python3 -m pyvirtualdisplay.examples.screenshot
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.threadsafe.txt b/doc/gen/python3_-m_pyvirtualdisplay.examples.threadsafe.txt
new file mode 100644
index 0000000..4202db0
--- /dev/null
+++ b/doc/gen/python3_-m_pyvirtualdisplay.examples.threadsafe.txt
@@ -0,0 +1 @@
+$ python3 -m pyvirtualdisplay.examples.threadsafe
diff --git a/doc/gen/python3_-m_pyvirtualdisplay.examples.vncserver.txt b/doc/gen/python3_-m_pyvirtualdisplay.examples.vncserver.txt
index ee0344d..ea757fd 100644
--- a/doc/gen/python3_-m_pyvirtualdisplay.examples.vncserver.txt
+++ b/doc/gen/python3_-m_pyvirtualdisplay.examples.vncserver.txt
@@ -1 +1 @@
-$ python3 -m pyvirtualdisplay.examples.vncserver
\ No newline at end of file
+$ python3 -m pyvirtualdisplay.examples.vncserver
diff --git a/doc/gen/vncviewer_localhost:5904.png b/doc/gen/vncviewer_localhost:5904.png
index 2be53c2..eed62b7 100644
Binary files a/doc/gen/vncviewer_localhost:5904.png and b/doc/gen/vncviewer_localhost:5904.png differ
diff --git a/doc/gen/vncviewer_localhost:5904.txt b/doc/gen/vncviewer_localhost:5904.txt
index 2b29074..1bf994b 100644
--- a/doc/gen/vncviewer_localhost:5904.txt
+++ b/doc/gen/vncviewer_localhost:5904.txt
@@ -1 +1 @@
-$ vncviewer localhost:5904
\ No newline at end of file
+$ vncviewer localhost:5904
diff --git a/doc/gen/xmessage1.png b/doc/gen/xmessage1.png
new file mode 100644
index 0000000..fec367a
Binary files /dev/null and b/doc/gen/xmessage1.png differ
diff --git a/doc/gen/xmessage2.png b/doc/gen/xmessage2.png
new file mode 100644
index 0000000..0247154
Binary files /dev/null and b/doc/gen/xmessage2.png differ
diff --git a/doc/generate-doc.py b/doc/generate-doc.py
index b77de07..3de0549 100644
--- a/doc/generate-doc.py
+++ b/doc/generate-doc.py
@@ -38,6 +38,7 @@ def empty_dir(dir):
 
 @entrypoint
 def main():
+    EasyProcess("killall Xvnc").call()
     gendir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gen")
     logging.info("gendir: %s", gendir)
     os.makedirs(gendir, exist_ok=True)
@@ -46,6 +47,8 @@ def main():
     try:
         os.chdir("gen")
         for cmd, grab, bg in commands:
+            sleep(1)
+
             with SmartDisplay() as disp:
                 logging.info("======== cmd: %s", cmd)
                 fname_base = cmd.replace(" ", "_")
@@ -64,15 +67,16 @@ def main():
                 if grab:
                     png = fname_base + ".png"
                     sleep(1)
-                    img = disp.waitgrab(timeout=9)
+                    img = disp.waitgrab()
                     logging.info("saving %s", png)
                     img.save(png)
     finally:
         os.chdir("..")
-        for p in pls:
+        for p in reversed(pls):
             p.stop()
+        EasyProcess("killall Xvnc").call()
     embedme = EasyProcess(["npx", "embedme", "../README.md"])
     embedme.call()
     print(embedme.stdout)
     assert embedme.return_code == 0
-    assert not "but file does not exist" in embedme.stdout
+    assert "but file does not exist" not in embedme.stdout
diff --git a/format-code.sh b/format-code.sh
index 0a1599c..12b68bf 100755
--- a/format-code.sh
+++ b/format-code.sh
@@ -1,6 +1,6 @@
-#!/bin/sh
+#!/bin/bash
 set -e
 autoflake  -i -r --remove-all-unused-imports .
 autoflake  -i -r --remove-unused-variables .
-isort --recursive .
+isort .
 black .
diff --git a/lint.sh b/lint.sh
new file mode 100755
index 0000000..ef40cbe
--- /dev/null
+++ b/lint.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+# TODO: max-complexity=10
+python3 -m flake8 . --max-complexity=11 --max-line-length=127 --extend-ignore=E203
+python3 -m mypy "pyvirtualdisplay"
diff --git a/pyvirtualdisplay/about.py b/pyvirtualdisplay/about.py
index d3c8880..fd24f38 100644
--- a/pyvirtualdisplay/about.py
+++ b/pyvirtualdisplay/about.py
@@ -1 +1 @@
-__version__ = "2.2"
+__version__ = "3.0"
diff --git a/pyvirtualdisplay/abstractdisplay.py b/pyvirtualdisplay/abstractdisplay.py
index aa7bb33..dd93943 100644
--- a/pyvirtualdisplay/abstractdisplay.py
+++ b/pyvirtualdisplay/abstractdisplay.py
@@ -2,14 +2,11 @@ import fnmatch
 import logging
 import os
 import select
-import signal
 import subprocess
 import tempfile
 import time
 from threading import Lock
 
-from easyprocess import EasyProcess, EasyProcessError
-
 from pyvirtualdisplay import xauth
 from pyvirtualdisplay.util import get_helptext, platform_is_osx
 
@@ -244,17 +241,34 @@ class AbstractDisplay(object):
                 break
 
             try:
-                xdpyinfo = EasyProcess(["xdpyinfo"], env=self._env())
-                xdpyinfo.enable_stdout_log = False
-                xdpyinfo.enable_stderr_log = False
-                exit_code = xdpyinfo.call().return_code
-            except EasyProcessError:
+                xdpyinfo = subprocess.Popen(
+                    ["xdpyinfo"],
+                    env=self._env(),
+                    stdout=subprocess.PIPE,
+                    stderr=subprocess.PIPE,
+                    shell=False,
+                )
+                _, _ = xdpyinfo.communicate()
+                exit_code = xdpyinfo.returncode
+            except FileNotFoundError:
                 log.warning(
                     "xdpyinfo was not found, X start can not be checked! Please install xdpyinfo!"
                 )
                 time.sleep(_X_START_WAIT)  # old method
                 ok = True
                 break
+            # try:
+            #     xdpyinfo = EasyProcess(["xdpyinfo"], env=self._env())
+            #     xdpyinfo.enable_stdout_log = False
+            #     xdpyinfo.enable_stderr_log = False
+            #     exit_code = xdpyinfo.call().return_code
+            # except EasyProcessError:
+            #     log.warning(
+            #         "xdpyinfo was not found, X start can not be checked! Please install xdpyinfo!"
+            #     )
+            #     time.sleep(_X_START_WAIT)  # old method
+            #     ok = True
+            #     break
 
             if exit_code != 0:
                 pass
@@ -276,7 +290,7 @@ class AbstractDisplay(object):
 
     def _wait_for_pipe_text(self, rfd):
         s = ""
-        # start_time = time.time()
+        start_time = time.time()
         while True:
             (rfd_changed_ls, _, _) = select.select([rfd], [], [], 0.1)
             if not self.is_alive():
@@ -289,11 +303,16 @@ class AbstractDisplay(object):
                 if c == b"\n":
                     break
                 s += c.decode("ascii")
-            # if time.time() - start_time >= _X_START_TIMEOUT:
-            #     raise XStartTimeoutError(
-            #         "No reply from program %s. command:%s"
-            #         % (self._program, self._command,)
-            #     )
+
+            # this timeout is for "eternal" hang. see #62
+            if time.time() - start_time >= 600:  # = 10 minutes
+                raise XStartTimeoutError(
+                    "No reply from program %s. command:%s"
+                    % (
+                        self._program,
+                        self._command,
+                    )
+                )
         return s
 
     def stop(self):
@@ -310,10 +329,7 @@ class AbstractDisplay(object):
 
         if self.is_alive():
             try:
-                try:
-                    self._subproc.terminate()
-                except AttributeError:
-                    os.kill(self._subproc.pid, signal.SIGKILL)
+                self._subproc.kill()
             except OSError as oserror:
                 log.debug("exception in terminate:%s", oserror)
 
@@ -372,17 +388,22 @@ class AbstractDisplay(object):
     def is_alive(self):
         if not self._subproc:
             return False
-        return self.return_code is None
-
-    @property
-    def return_code(self):
-        if not self._subproc:
-            return None
+        # return self.return_code is None
         rc = self._subproc.poll()
         if rc is not None:
             # proc exited
             self._read_stdout_stderr()
-        return rc
+        return rc is None
+
+    # @property
+    # def return_code(self):
+    #     if not self._subproc:
+    #         return None
+    #     rc = self._subproc.poll()
+    #     if rc is not None:
+    #         # proc exited
+    #         self._read_stdout_stderr()
+    #     return rc
 
     @property
     def pid(self):
diff --git a/pyvirtualdisplay/display.py b/pyvirtualdisplay/display.py
index 5b55147..afde275 100644
--- a/pyvirtualdisplay/display.py
+++ b/pyvirtualdisplay/display.py
@@ -17,7 +17,7 @@ class Display(object):
     :param visible: True -> Xephyr, False -> Xvfb
     :param backend: 'xvfb', 'xvnc' or 'xephyr', ignores ``visible``
     :param xauth: If a Xauthority file should be created.
-    :param manage_global_env: if True then $DISPLAY is set in os.environ 
+    :param manage_global_env: if True then $DISPLAY is set in os.environ
         which is not thread-safe. Use False to make it thread-safe.
     """
 
@@ -93,9 +93,9 @@ class Display(object):
     def is_alive(self) -> bool:
         return self._obj.is_alive()
 
-    @property
-    def return_code(self):
-        return self._obj.return_code
+    # @property
+    # def return_code(self):
+    #     return self._obj.return_code
 
     @property
     def pid(self) -> int:
@@ -108,12 +108,12 @@ class Display(object):
 
     @property
     def display(self) -> int:
-        """The new $DISPLAY variable as int.  Example 1 if $DISPLAY=':1'  """
+        """The new $DISPLAY variable as int.  Example 1 if $DISPLAY=':1'"""
         return self._obj.display
 
     @property
     def new_display_var(self) -> str:
-        """The new $DISPLAY variable like ':1'  """
+        """The new $DISPLAY variable like ':1'"""
         return self._obj.new_display_var
 
     def env(self) -> Dict[str, str]:
diff --git a/pyvirtualdisplay/examples/threadsafe.py b/pyvirtualdisplay/examples/threadsafe.py
index e353589..b15576c 100644
--- a/pyvirtualdisplay/examples/threadsafe.py
+++ b/pyvirtualdisplay/examples/threadsafe.py
@@ -1,17 +1,26 @@
-"Start Xvfb server. Open xmessage window. Thread safe."
+"Start Xvfb server and open xmessage window. Thread safe."
+
+import threading
 
 from easyprocess import EasyProcess
 
-from pyvirtualdisplay import Display
+from pyvirtualdisplay.smartdisplay import SmartDisplay
+
 
-# manage_global_env=False is thread safe
-with Display(manage_global_env=False) as disp:
-    # disp.new_display_var should be used for new processes
-    print("disp.new_display_var=" + disp.new_display_var)
+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))
 
-    # disp.env() copies global os.environ and adds disp.new_display_var
-    print("disp.env()['DISPLAY']=" + disp.env()["DISPLAY"])
 
-    # set $DISPLAY for subprocesses
-    with EasyProcess(["xmessage", "-timeout", "1", "hello"], env=disp.env()) as proc:
-        proc.wait()
+t1 = threading.Thread(target=thread_function, args=(1,))
+t2 = threading.Thread(target=thread_function, args=(2,))
+t1.start()
+t2.start()
+t1.join()
+t2.join()
diff --git a/pyvirtualdisplay/py.typed b/pyvirtualdisplay/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyvirtualdisplay/smartdisplay.py b/pyvirtualdisplay/smartdisplay.py
index de3f32e..a4cbba4 100644
--- a/pyvirtualdisplay/smartdisplay.py
+++ b/pyvirtualdisplay/smartdisplay.py
@@ -16,9 +16,9 @@ class DisplayTimeoutError(Exception):
 def autocrop(im, bgcolor):
     """Crop borders off an image.
 
-        :param im: Source image.
-        :param bgcolor: Background color, using either a color tuple.
-        :return: An image without borders, or None if there's no actual content in the image.
+    :param im: Source image.
+    :param bgcolor: Background color, using either a color tuple.
+    :return: An image without borders, or None if there's no actual content in the image.
     """
     if im.mode != "RGB":
         im = im.convert("RGB")
diff --git a/pyvirtualdisplay/util.py b/pyvirtualdisplay/util.py
index 86472ff..a0e3d9f 100644
--- a/pyvirtualdisplay/util.py
+++ b/pyvirtualdisplay/util.py
@@ -1,14 +1,24 @@
+import subprocess
 import sys
 
-from easyprocess import EasyProcess
-
 
 def get_helptext(program):
-    p = EasyProcess([program, "-help"])
-    p.enable_stdout_log = False
-    p.enable_stderr_log = False
-    p.call()
-    helptext = p.stderr
+    cmd = [program, "-help"]
+
+    # py3.7+
+    # p = subprocess.run(cmd, capture_output=True)
+    # stderr = p.stderr
+
+    # py3.6 also
+    p = subprocess.Popen(
+        cmd,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        shell=False,
+    )
+    _, stderr = p.communicate()
+
+    helptext = stderr.decode("utf-8", "ignore")
     return helptext
 
 
diff --git a/pyvirtualdisplay/xauth.py b/pyvirtualdisplay/xauth.py
index bc50368..ce0f804 100644
--- a/pyvirtualdisplay/xauth.py
+++ b/pyvirtualdisplay/xauth.py
@@ -1,8 +1,7 @@
 """Utility functions for xauth."""
 import hashlib
 import os
-
-from easyprocess import EasyProcess
+import subprocess
 
 
 class NotFoundError(Exception):
@@ -14,11 +13,18 @@ def is_installed():
     Return whether or not xauth is installed.
     """
     try:
-        p = EasyProcess(["xauth", "-V"])
-        p.enable_stdout_log = False
-        p.enable_stderr_log = False
-        p.call()
-    except Exception:
+        xauth = subprocess.Popen(
+            ["xauth", "-V"],
+            # env=self._env(),
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        _, _ = xauth.communicate()
+        # p = EasyProcess(["xauth", "-V"])
+        # p.enable_stdout_log = False
+        # p.enable_stderr_log = False
+        # p.call()
+    except FileNotFoundError:
         return False
     else:
         return True
@@ -36,4 +42,11 @@ def call(*args):
     """
     Call xauth with the given args.
     """
-    EasyProcess(["xauth"] + list(args)).call()
+    xauth = subprocess.Popen(
+        ["xauth"] + list(args),
+        # env=self._env(),
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+    )
+    _, _ = xauth.communicate()
+    # EasyProcess(["xauth"] + list(args)).call()
diff --git a/pyvirtualdisplay/xephyr.py b/pyvirtualdisplay/xephyr.py
index 815151f..6348e69 100644
--- a/pyvirtualdisplay/xephyr.py
+++ b/pyvirtualdisplay/xephyr.py
@@ -46,13 +46,17 @@ class XephyrDisplay(AbstractDisplay):
         self._has_resizeable = "-resizeable" in helptext
 
     def _cmd(self):
-        cmd = [
-            PROGRAM,
-        ] + (["-parent", self._parent] if self._parent else []) + [
-            dict(black="-br", white="-wr")[self._bgcolor],
-            "-screen",
-            "x".join(map(str, list(self._size) + [self._color_depth])),
-        ]
+        cmd = (
+            [
+                PROGRAM,
+            ]
+            + (["-parent", self._parent] if self._parent else [])
+            + [
+                dict(black="-br", white="-wr")[self._bgcolor],
+                "-screen",
+                "x".join(map(str, list(self._size) + [self._color_depth])),
+            ]
+        )
         if self._has_displayfd:
             cmd += ["-displayfd", str(self._pipe_wfd)]
         else:
diff --git a/pyvirtualdisplay/xvnc.py b/pyvirtualdisplay/xvnc.py
index aeb61db..1f4c1fc 100644
--- a/pyvirtualdisplay/xvnc.py
+++ b/pyvirtualdisplay/xvnc.py
@@ -26,7 +26,9 @@ class XvncDisplay(AbstractDisplay):
     ):
         """
         :param bgcolor: 'black' or 'white'
-        :param rfbport: Specifies the TCP port on which Xvnc listens for connections from viewers (the protocol used in VNC is called RFB - "remote framebuffer"). The default is 5900 plus the display number.
+        :param rfbport: Specifies the TCP port on which Xvnc listens for connections from viewers
+        (the protocol used in VNC is called RFB - "remote framebuffer").
+        The default is 5900 plus the display number.
         :param rfbauth: Specifies the file containing the password used to authenticate viewers.
         """
         self._size = size
diff --git a/requirements-doc.txt b/requirements-doc.txt
new file mode 100644
index 0000000..ac5fc9c
--- /dev/null
+++ b/requirements-doc.txt
@@ -0,0 +1,13 @@
+autoflake
+isort
+black
+
+# pytest
+pillow
+entrypoint2
+vncdotool==0.13.0
+# psutil
+# for travis xenial
+# attrs
+# pytest-xdist
+EasyProcess
diff --git a/requirements-test.txt b/requirements-test.txt
index c87754c..f34c626 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,8 +1,12 @@
 pytest
 pillow
+types-pillow
 entrypoint2
 vncdotool==0.13.0
 psutil
 # for travis xenial
 attrs
-pytest-xdist
\ No newline at end of file
+pytest-xdist
+mypy
+flake8
+EasyProcess
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 1b80e87..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-EasyProcess
-
-
diff --git a/setup.py b/setup.py
index 6b16d75..47053c2 100644
--- a/setup.py
+++ b/setup.py
@@ -35,9 +35,9 @@ classifiers = [
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
 ]
 
-install_requires = ["EasyProcess"]
 
 setup(
     name=PYPI_NAME,
@@ -52,8 +52,8 @@ setup(
     url=URL,
     license="BSD",
     packages=PACKAGES,
-    #     include_package_data=True,
-    #     zip_safe=False,
-    install_requires=install_requires,
-    # **extra
+    # install_requires=install_requires,
+    package_data={
+        NAME: ["py.typed"],
+    },
 )
diff --git a/tests/test_core.py b/tests/test_core.py
index 302a211..6d5d42d 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,28 +1,28 @@
 from time import sleep
 
 import pytest
+from tutil import has_xvnc, rfbport
 
 from pyvirtualdisplay import Display
 from pyvirtualdisplay.abstractdisplay import XStartError
 from pyvirtualdisplay.xephyr import XephyrDisplay
 from pyvirtualdisplay.xvfb import XvfbDisplay
 from pyvirtualdisplay.xvnc import XvncDisplay
-from tutil import has_xvnc, rfbport
 
 
 def test_virt():
     vd = Display()
-    assert vd.return_code is None
+    # assert vd.return_code is None
     assert not vd.is_alive()
     vd.start()
-    assert vd.return_code is None
+    # assert vd.return_code is None
     assert vd.is_alive()
     vd.stop()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
     vd = Display().start().stop()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
 
@@ -32,7 +32,8 @@ def test_nest():
 
     nd = Display(visible=True).start().stop()
 
-    assert nd.return_code == 0
+    # assert nd.return_code == 0
+    assert not nd.is_alive()
 
     vd.stop()
     assert not vd.is_alive()
@@ -46,7 +47,8 @@ def test_disp():
     # .assertEquals(d.return_code, 0)
 
     d = Display(visible=False).start().stop()
-    assert d.return_code == 0
+    # assert d.return_code == 0
+    assert not d.is_alive()
 
     vd.stop()
     assert not vd.is_alive()
@@ -104,21 +106,21 @@ def test_double_start():
 
 def test_double_stop():
     vd = Display().start().stop()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
     vd.stop()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
 
 def test_stop_terminated():
     vd = Display().start()
     assert vd.is_alive()
-    vd._obj._subproc.terminate()
-    sleep(0.2)
+    vd._obj._subproc.kill()
+    sleep(1)
     assert not vd.is_alive()
     vd.stop()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
 
diff --git a/tests/test_examples.py b/tests/test_examples.py
index c814973..1f6cfe0 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -5,9 +5,9 @@ from tempfile import TemporaryDirectory
 from time import sleep
 
 from easyprocess import EasyProcess
+from tutil import has_xvnc, kill_process_tree, prog_check, worker
 
 from pyvirtualdisplay import Display
-from tutil import has_xvnc, kill_process_tree, prog_check, worker
 
 log = logging.getLogger(__name__)
 
diff --git a/tests/test_race.py b/tests/test_race.py
index e285a4e..1de6625 100644
--- a/tests/test_race.py
+++ b/tests/test_race.py
@@ -3,9 +3,9 @@ from time import sleep
 
 from easyprocess import EasyProcess
 from entrypoint2 import entrypoint
+from tutil import has_xvnc, worker
 
 from pyvirtualdisplay import Display
-from tutil import has_xvnc, worker
 
 # ubuntu 14.04 no displayfd
 # ubuntu 16.04 displayfd
@@ -26,7 +26,6 @@ def test_race_100_xvfb():
 #         check_n(100, "xephyr")
 
 
-
 if has_xvnc():
 
     def test_race_10_xvnc():
@@ -77,7 +76,12 @@ def main(i, backend, retries):
     d = Display(backend=backend, retries=retries, **kwargs).start()
     print(
         "my index:%s  backend:%s disp:%s retries:%s"
-        % (i, backend, d.new_display_var, d._obj._retries_current,)
+        % (
+            i,
+            backend,
+            d.new_display_var,
+            d._obj._retries_current,
+        )
     )
     ok = d.is_alive()
     d.stop()
diff --git a/tests/test_smart.py b/tests/test_smart.py
index 340a175..89968c4 100644
--- a/tests/test_smart.py
+++ b/tests/test_smart.py
@@ -14,10 +14,12 @@ def test_disp():
     with Display():
 
         d = SmartDisplay(visible=True).start().stop()
-        assert d.return_code == 0
+        # assert d.return_code == 0
+        assert not d.is_alive()
 
         d = SmartDisplay(visible=False).start().stop()
-        assert d.return_code == 0
+        # assert d.return_code == 0
+        assert not d.is_alive()
 
 
 def test_slowshot():
@@ -75,7 +77,7 @@ def test_slowshot_timeout():
     with disp:
         with proc:
             with pytest.raises(DisplayTimeoutError):
-                img = disp.waitgrab(timeout=1)
+                disp.waitgrab(timeout=1)
 
 
 def test_slowshot_timeout_nocrop():
@@ -85,4 +87,4 @@ def test_slowshot_timeout_nocrop():
     with disp:
         with proc:
             with pytest.raises(DisplayTimeoutError):
-                img = disp.waitgrab(timeout=1, autocrop=False)
+                disp.waitgrab(timeout=1, autocrop=False)
diff --git a/tests/test_with.py b/tests/test_with.py
index 9b3b162..25dad96 100644
--- a/tests/test_with.py
+++ b/tests/test_with.py
@@ -1,24 +1,25 @@
-from pyvirtualdisplay import Display
 from tutil import has_xvnc, rfbport
 
+from pyvirtualdisplay import Display
+
 
 def test_with_xvfb():
     with Display(size=(800, 600)) as vd:
         assert vd.is_alive()
         assert vd._backend == "xvfb"
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
     with Display(visible=False, size=(800, 600)) as vd:
         assert vd.is_alive()
         assert vd._backend == "xvfb"
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
     with Display(backend="xvfb", size=(800, 600)) as vd:
         assert vd.is_alive()
         assert vd._backend == "xvfb"
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
 
 
@@ -27,13 +28,13 @@ def test_with_xephyr():
         with Display(visible=True, size=(800, 600)) as vd:
             assert vd.is_alive()
             assert vd._backend == "xephyr"
-        assert vd.return_code == 0
+        # assert vd.return_code == 0
         assert not vd.is_alive()
 
         with Display(backend="xephyr", size=(800, 600)) as vd:
             assert vd.is_alive()
             assert vd._backend == "xephyr"
-        assert vd.return_code == 0
+        # assert vd.return_code == 0
         assert not vd.is_alive()
 
 
@@ -43,12 +44,12 @@ if has_xvnc():
         with Display(backend="xvnc", size=(800, 600), rfbport=rfbport()) as vd:
             assert vd.is_alive()
             assert vd._backend == "xvnc"
-        assert vd.return_code == 0
+        # assert vd.return_code == 0
         assert not vd.is_alive()
 
 
 def test_dpi():
     with Display(backend="xvfb", size=(800, 600), dpi=99) as vd:
         assert vd.is_alive()
-    assert vd.return_code == 0
+    # assert vd.return_code == 0
     assert not vd.is_alive()
diff --git a/tests/test_xauth.py b/tests/test_xauth.py
index 834b97b..a26d738 100644
--- a/tests/test_xauth.py
+++ b/tests/test_xauth.py
@@ -1,26 +1,29 @@
 import os
 
+from tutil import prog_check
+
 from pyvirtualdisplay import Display, xauth
 
+if prog_check(["xauth", "-V"]):
+
+    def test_xauth_is_installed():
+        assert xauth.is_installed()
 
-def test_xauth():
-    """
-    Test that a Xauthority file is created.
-    """
-    if not xauth.is_installed():
-        print("This test needs xauth installed")
-        return
-    old_xauth = os.getenv("XAUTHORITY")
-    display = Display(visible=False, use_xauth=True)
-    display.start()
-    new_xauth = os.getenv("XAUTHORITY")
+    def test_xauth():
+        """
+        Test that a Xauthority file is created.
+        """
+        old_xauth = os.getenv("XAUTHORITY")
+        display = Display(visible=False, use_xauth=True)
+        display.start()
+        new_xauth = os.getenv("XAUTHORITY")
 
-    assert new_xauth is not None
-    assert os.path.isfile(new_xauth)
-    filename = os.path.basename(new_xauth)
-    assert filename.startswith("PyVirtualDisplay.")
-    assert filename.endswith("Xauthority")
+        assert new_xauth is not None
+        assert os.path.isfile(new_xauth)
+        filename = os.path.basename(new_xauth)
+        assert filename.startswith("PyVirtualDisplay.")
+        assert filename.endswith("Xauthority")
 
-    display.stop()
-    assert old_xauth == os.getenv("XAUTHORITY")
-    assert not os.path.isfile(new_xauth)
+        display.stop()
+        assert old_xauth == os.getenv("XAUTHORITY")
+        assert not os.path.isfile(new_xauth)
diff --git a/tests/test_xvnc.py b/tests/test_xvnc.py
index 91e72f0..cf04b33 100644
--- a/tests/test_xvnc.py
+++ b/tests/test_xvnc.py
@@ -1,11 +1,11 @@
 import tempfile
 from pathlib import Path
 
+from tutil import has_xvnc, rfbport, worker
 from vncdotool import api
 
 from pyvirtualdisplay import Display
 from pyvirtualdisplay.xvnc import XvncDisplay
-from tutil import has_xvnc, rfbport, worker
 
 if has_xvnc():
 
diff --git a/tests/vagrant/Vagrantfile.debian10.rb b/tests/vagrant/Vagrantfile.debian10.rb
new file mode 100644
index 0000000..b3a7671
--- /dev/null
+++ b/tests/vagrant/Vagrantfile.debian10.rb
@@ -0,0 +1,17 @@
+Vagrant.configure("2") do |config|
+  config.vm.box = "debian/buster64"
+  config.vm.boot_timeout = 600
+
+  config.vm.provider "virtualbox" do |vb|
+    #  vb.gui = true
+    vb.memory = "2048"
+    vb.name = "pyvirtualdisplay_debian10"
+  end
+
+  config.vm.provision "shell", path: "tests/vagrant/debian10.sh", privileged: true
+
+  config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]
+end
+
+# export VAGRANT_VAGRANTFILE=tests/vagrant/Vagrantfile.debian10.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
+# vagrant up && vagrant ssh
diff --git a/tests/vagrant/Vagrantfile.debian11.rb b/tests/vagrant/Vagrantfile.debian11.rb
new file mode 100644
index 0000000..c4db201
--- /dev/null
+++ b/tests/vagrant/Vagrantfile.debian11.rb
@@ -0,0 +1,17 @@
+Vagrant.configure("2") do |config|
+  config.vm.box = "debian/bullseye64"
+  config.vm.boot_timeout = 600
+
+  config.vm.provider "virtualbox" do |vb|
+    #  vb.gui = true
+    vb.memory = "2048"
+    vb.name = "pyvirtualdisplay_debian11"
+  end
+
+  config.vm.provision "shell", path: "tests/vagrant/debian11.sh", privileged: true
+
+  config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]
+end
+
+# export VAGRANT_VAGRANTFILE=tests/vagrant/Vagrantfile.debian11.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
+# vagrant up && vagrant ssh
diff --git a/tests/vagrant/Vagrantfile.ubuntu1804.rb b/tests/vagrant/Vagrantfile.ubuntu1804.rb
new file mode 100644
index 0000000..1c169ad
--- /dev/null
+++ b/tests/vagrant/Vagrantfile.ubuntu1804.rb
@@ -0,0 +1,16 @@
+Vagrant.configure(2) do |config|
+  config.vm.box = "ubuntu/bionic64"
+
+  config.vm.provider "virtualbox" do |vb|
+    vb.name = "pyvirtualdisplay_ubuntu1804"
+    #   vb.gui = true
+    vb.memory = "2048" # ste high because of Xephyr memory leak
+  end
+
+  config.vm.provision "shell", path: "tests/vagrant/ubuntu1804.sh"
+
+  config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]
+end
+
+# export VAGRANT_VAGRANTFILE=tests/vagrant/Vagrantfile.18.04.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
+# vagrant up && vagrant ssh
diff --git a/tests/vagrant/Vagrantfile.ubuntu2204.rb b/tests/vagrant/Vagrantfile.ubuntu2204.rb
new file mode 100644
index 0000000..7c137a1
--- /dev/null
+++ b/tests/vagrant/Vagrantfile.ubuntu2204.rb
@@ -0,0 +1,17 @@
+Vagrant.configure(2) do |config|
+  config.vm.box = "ubuntu/jammy64"
+  config.vm.box_version = "20220104.0.0"
+
+  config.vm.provider "virtualbox" do |vb|
+    vb.name = "pyvirtualdisplay_ubuntu2204"
+    #   vb.gui = true
+    vb.memory = "2048" # ste high because of Xephyr memory leak
+  end
+
+  config.vm.provision "shell", path: "tests/vagrant/ubuntu2204.sh"
+
+  config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"]
+end
+
+# export VAGRANT_VAGRANTFILE=tests/vagrant/Vagrantfile.22.04.rb;export VAGRANT_DOTFILE_PATH=.vagrant_${VAGRANT_VAGRANTFILE}
+# vagrant up && vagrant ssh
diff --git a/tests/vagrant/debian10.sh b/tests/vagrant/debian10.sh
new file mode 100644
index 0000000..f907765
--- /dev/null
+++ b/tests/vagrant/debian10.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+export DEBIAN_FRONTEND=noninteractive
+sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8
+# echo 'export export LC_ALL=C' >> /home/vagrant/.profile
+
+# install python versions
+# sudo add-apt-repository --yes ppa:deadsnakes/ppa
+sudo apt-get update
+# sudo apt-get install -y python3.6-dev
+# sudo apt-get install -y python3.7-dev
+# sudo apt-get install -y python3.8-dev
+# sudo apt-get install -y python3-distutils
+# sudo apt-get install -y python3.9-dev
+# sudo apt-get install -y python3.9-distutils
+# sudo apt-get install -y python3.10-dev
+# sudo apt-get install -y python3.10-distutils
+
+# tools
+sudo apt-get install -y mc python3-pip xvfb
+
+# for pillow source install
+#  sudo apt-get install -y libjpeg-dev zlib1g-dev
+
+# project dependencies
+sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
+
+# test dependencies
+sudo apt-get install -y gnumeric
+sudo apt-get install -y x11-utils #   for: xmessage
+# sudo apt-get install -y x11-apps  #   for: xlogo
+sudo pip3 install tox
+
+# doc dependencies
+# sudo apt-get install -y npm xtightvncviewer
+# sudo npm install -g npx
+#  sudo pip install -r /vagrant/requirements-doc.txt
diff --git a/tests/vagrant/debian11.sh b/tests/vagrant/debian11.sh
new file mode 100644
index 0000000..f907765
--- /dev/null
+++ b/tests/vagrant/debian11.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+export DEBIAN_FRONTEND=noninteractive
+sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8
+# echo 'export export LC_ALL=C' >> /home/vagrant/.profile
+
+# install python versions
+# sudo add-apt-repository --yes ppa:deadsnakes/ppa
+sudo apt-get update
+# sudo apt-get install -y python3.6-dev
+# sudo apt-get install -y python3.7-dev
+# sudo apt-get install -y python3.8-dev
+# sudo apt-get install -y python3-distutils
+# sudo apt-get install -y python3.9-dev
+# sudo apt-get install -y python3.9-distutils
+# sudo apt-get install -y python3.10-dev
+# sudo apt-get install -y python3.10-distutils
+
+# tools
+sudo apt-get install -y mc python3-pip xvfb
+
+# for pillow source install
+#  sudo apt-get install -y libjpeg-dev zlib1g-dev
+
+# project dependencies
+sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
+
+# test dependencies
+sudo apt-get install -y gnumeric
+sudo apt-get install -y x11-utils #   for: xmessage
+# sudo apt-get install -y x11-apps  #   for: xlogo
+sudo pip3 install tox
+
+# doc dependencies
+# sudo apt-get install -y npm xtightvncviewer
+# sudo npm install -g npx
+#  sudo pip install -r /vagrant/requirements-doc.txt
diff --git a/tests/vagrant/osx.sh b/tests/vagrant/osx.sh
index 906cafe..74d7311 100755
--- a/tests/vagrant/osx.sh
+++ b/tests/vagrant/osx.sh
@@ -1,4 +1,5 @@
-#!/bin/sh
+#!/bin/bash
+set -e
 
 #autologin
 brew tap xfreebird/utils
@@ -17,7 +18,14 @@ sudo systemsetup -setharddisksleep Never
 echo  "@reboot /bin/sh -c 'mkdir /tmp/.X11-unix;sudo chmod 1777 /tmp/.X11-unix;sudo chown root /tmp/.X11-unix/'" > mycron
 sudo crontab mycron
 
-brew install python3 mc pidof
+# Error: 
+#  homebrew-core is a shallow clone.
+# To `brew update`, first run:
+git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow
+
+brew install openssl@1.1
+brew install python3 
+brew install pidof
 brew install --cask xquartz
 # TODO: xvnc install
 python3 -m pip install --user pillow  pytest tox
diff --git a/tests/vagrant/ubuntu1804.sh b/tests/vagrant/ubuntu1804.sh
new file mode 100644
index 0000000..610c4a1
--- /dev/null
+++ b/tests/vagrant/ubuntu1804.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+export DEBIAN_FRONTEND=noninteractive
+sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8
+# echo 'export export LC_ALL=C' >> /home/vagrant/.profile
+
+# install python versions
+sudo add-apt-repository --yes ppa:deadsnakes/ppa
+sudo apt-get update
+sudo apt-get install -y python3.6-dev
+sudo apt-get install -y python3.7-dev
+sudo apt-get install -y python3.8-dev
+sudo apt-get install -y python3-distutils
+sudo apt-get install -y python3.9-dev
+sudo apt-get install -y python3.9-distutils
+sudo apt-get install -y python3.10-dev
+sudo apt-get install -y python3.10-distutils
+
+# tools
+sudo apt-get install -y mc python3-pip xvfb
+
+# for pillow source install
+#  sudo apt-get install -y libjpeg-dev zlib1g-dev
+
+# project dependencies
+sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
+
+# test dependencies
+sudo apt-get install -y gnumeric
+sudo apt-get install -y x11-utils #   for: xmessage
+# sudo apt-get install -y x11-apps  #   for: xlogo
+sudo pip3 install tox
+
+# doc dependencies
+sudo apt-get install -y npm xtightvncviewer
+sudo npm install -g npx
+#  sudo pip install -r /vagrant/requirements-doc.txt
diff --git a/tests/vagrant/ubuntu2004.sh b/tests/vagrant/ubuntu2004.sh
new file mode 100644
index 0000000..610c4a1
--- /dev/null
+++ b/tests/vagrant/ubuntu2004.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+export DEBIAN_FRONTEND=noninteractive
+sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8
+# echo 'export export LC_ALL=C' >> /home/vagrant/.profile
+
+# install python versions
+sudo add-apt-repository --yes ppa:deadsnakes/ppa
+sudo apt-get update
+sudo apt-get install -y python3.6-dev
+sudo apt-get install -y python3.7-dev
+sudo apt-get install -y python3.8-dev
+sudo apt-get install -y python3-distutils
+sudo apt-get install -y python3.9-dev
+sudo apt-get install -y python3.9-distutils
+sudo apt-get install -y python3.10-dev
+sudo apt-get install -y python3.10-distutils
+
+# tools
+sudo apt-get install -y mc python3-pip xvfb
+
+# for pillow source install
+#  sudo apt-get install -y libjpeg-dev zlib1g-dev
+
+# project dependencies
+sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
+
+# test dependencies
+sudo apt-get install -y gnumeric
+sudo apt-get install -y x11-utils #   for: xmessage
+# sudo apt-get install -y x11-apps  #   for: xlogo
+sudo pip3 install tox
+
+# doc dependencies
+sudo apt-get install -y npm xtightvncviewer
+sudo npm install -g npx
+#  sudo pip install -r /vagrant/requirements-doc.txt
diff --git a/tests/vagrant/ubuntu2204.sh b/tests/vagrant/ubuntu2204.sh
new file mode 100644
index 0000000..5812640
--- /dev/null
+++ b/tests/vagrant/ubuntu2204.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+export DEBIAN_FRONTEND=noninteractive
+sudo update-locale LANG=en_US.UTF-8 LANGUAGE=en.UTF-8
+# echo 'export export LC_ALL=C' >> /home/vagrant/.profile
+
+# install python versions
+# sudo add-apt-repository --yes ppa:deadsnakes/ppa
+sudo apt-get update
+# sudo apt-get install -y python3.6-dev
+# sudo apt-get install -y python3.7-dev
+# sudo apt-get install -y python3.8-dev
+# sudo apt-get install -y python3-distutils
+# sudo apt-get install -y python3.9-dev
+# sudo apt-get install -y python3.9-distutils
+# sudo apt-get install -y python3.10-dev
+# sudo apt-get install -y python3.10-distutils
+
+# tools
+sudo apt-get install -y mc python3-pip xvfb
+
+# for pillow source install
+#  sudo apt-get install -y libjpeg-dev zlib1g-dev
+
+# project dependencies
+sudo apt-get install -y xvfb xserver-xephyr tigervnc-standalone-server
+
+# test dependencies
+sudo apt-get install -y gnumeric
+sudo apt-get install -y x11-utils #   for: xmessage
+# sudo apt-get install -y x11-apps  #   for: xlogo
+sudo pip3 install tox
+
+# doc dependencies
+sudo apt-get install -y npm xtightvncviewer
+sudo npm install -g npx
+#  sudo pip install -r /vagrant/requirements-doc.txt
diff --git a/tests/vagrant/vagrant_boxes.py b/tests/vagrant/vagrant_boxes.py
index 1fae720..9562d6e 100755
--- a/tests/vagrant/vagrant_boxes.py
+++ b/tests/vagrant/vagrant_boxes.py
@@ -4,12 +4,13 @@ from pathlib import Path
 from time import sleep
 
 import fabric
-import vagrant
 from entrypoint2 import entrypoint
 
+import vagrant
+
 # pip3 install fabric vncdotool python-vagrant entrypoint2
 
-DIR = Path(__file__).parent.parent.parent
+DIR = Path(__file__).parent
 
 
 class Options:
@@ -20,11 +21,12 @@ class Options:
 
 def run_box(options, vagrantfile, cmds):
     env = os.environ
-    env["VAGRANT_VAGRANTFILE"] = str(DIR / vagrantfile)
-    if vagrantfile != "Vagrantfile":
-        env["VAGRANT_DOTFILE_PATH"] = str(DIR / (".vagrant_" + vagrantfile))
-    else:
+    if vagrantfile == "Vagrantfile":
+        env["VAGRANT_VAGRANTFILE"] = str(DIR.parent.parent / vagrantfile)
         env["VAGRANT_DOTFILE_PATH"] = ""
+    else:
+        env["VAGRANT_VAGRANTFILE"] = str(DIR / vagrantfile)
+        env["VAGRANT_DOTFILE_PATH"] = str(DIR / (".vagrant_" + vagrantfile))
 
     v = vagrant.Vagrant(env=env, quiet_stdout=False, quiet_stderr=False)
     status = v.status()
@@ -49,7 +51,10 @@ def run_box(options, vagrantfile, cmds):
         v.up()
 
         with fabric.Connection(
-            v.user_hostname_port(), connect_kwargs={"key_filename": v.keyfile(),},
+            v.user_hostname_port(),
+            connect_kwargs={
+                "key_filename": v.keyfile(),
+            },
         ) as conn:
             with conn.cd("c:/vagrant" if options.win else "/vagrant"):
                 if not options.win:
@@ -71,16 +76,33 @@ def run_box(options, vagrantfile, cmds):
 
 
 config = {
-    "server2004": ("Vagrantfile", ["tox", "PYVIRTUALDISPLAY_DISPLAYFD=0 tox"],),
-    "server1804": ("Vagrantfile.18.04.rb", ["tox"],),
-    "server1404": ("Vagrantfile.14.04.rb", ["tox -e py36"],),
-    "osx": (
-        "Vagrantfile.osx.rb",
-        [
-            "bash --login -c 'python3 -m tox -e py3-osx'",
-            # TODO: "bash --login -c 'PYVIRTUALDISPLAY_DISPLAYFD=0 python3 -m tox -e py3-osx'",
-        ],
+    "debian10": (
+        "Vagrantfile.debian10.rb",
+        ["tox -e py37"],
+    ),
+    "debian11": (
+        "Vagrantfile.debian11.rb",
+        ["tox -e py39"],
+    ),
+    "ubuntu2204": (
+        "Vagrantfile.ubuntu2204.rb",
+        ["tox -e py39"],
     ),
+    "ubuntu2004": (
+        "Vagrantfile",
+        ["tox", "PYVIRTUALDISPLAY_DISPLAYFD=0 tox"],
+    ),
+    "ubuntu1804": (
+        "Vagrantfile.ubuntu1804.rb",
+        ["tox -e py36"],
+    ),
+    # "osx": (
+    #     "Vagrantfile.osx.rb",
+    #     [
+    #         "bash --login -c 'python3 -m tox -e py3-osx'",
+    #         # TODO: "bash --login -c 'PYVIRTUALDISPLAY_DISPLAYFD=0 python3 -m tox -e py3-osx'",
+    #     ],
+    # ),
 }
 
 
@@ -95,9 +117,23 @@ def main(boxes="all", fast=False, destroy=False):
         boxes = list(config.keys())
     else:
         boxes = boxes.split(",")
+
     for k, v in config.items():
-        if k in boxes:
-            options.win = k == "win"
-            options.osx = k == "osx"
-            print("-----> %s %s %s" % (k, v[0], v[1]))
-            run_box(options, v[0], v[1])
+        name = k
+        vagrantfile, cmds = v[0], v[1]
+        if name in boxes:
+            options.win = k.startswith("win")
+            options.osx = k.startswith("osx")
+            print("----->")
+            print("----->")
+            print("-----> %s %s %s" % (name, vagrantfile, cmds))
+            print("----->")
+            print("----->")
+            try:
+                run_box(options, vagrantfile, cmds)
+            finally:
+                print("<-----")
+                print("<-----")
+                print("<----- %s %s %s" % (name, vagrantfile, cmds))
+                print("<-----")
+                print("<-----")
diff --git a/tox.ini b/tox.ini
index ce957cf..93105bb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,13 @@
 
 [tox]
 envlist = 
+      py310
       py39
       py38
       py37
       py36
+      py310-doc
+      py310-lint
 
 # Workaround for Vagrant
 #toxworkdir={toxinidir}/.tox # default
@@ -32,9 +35,20 @@ commands=
       {envpython} -m pytest -v .
 
 
-[testenv:py3-doc]
+[testenv:py310-doc]
+allowlist_externals=bash
 changedir=doc
-deps = -rrequirements-test.txt
+deps = 
+      -rrequirements-doc.txt
 
 commands=
+      bash -c "cd ..;./format-code.sh"
       {envpython} generate-doc.py --debug
+
+[testenv:py310-lint]
+allowlist_externals=bash
+changedir=.
+deps = -rrequirements-test.txt
+
+commands=
+      bash -c "./lint.sh"