pax_global_header00006660000000000000000000000064142017106530014511gustar00rootroot0000000000000052 comment=7f200dd0dbe3e60cd32f109a1bbfa0f3831ae93d PyVirtualDisplay-3.0/000077500000000000000000000000001420171065300146405ustar00rootroot00000000000000PyVirtualDisplay-3.0/.gitattributes000066400000000000000000000000521420171065300175300ustar00rootroot00000000000000pyvirtualdisplay/_version.py export-subst PyVirtualDisplay-3.0/.github/000077500000000000000000000000001420171065300162005ustar00rootroot00000000000000PyVirtualDisplay-3.0/.github/workflows/000077500000000000000000000000001420171065300202355ustar00rootroot00000000000000PyVirtualDisplay-3.0/.github/workflows/main.yml000066400000000000000000000040401420171065300217020ustar00rootroot00000000000000# 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 PyVirtualDisplay-3.0/.gitignore000066400000000000000000000005301420171065300166260ustar00rootroot00000000000000*.py[c|o] *.egg *.egg-info /build /dist /nbproject/ pip-log.txt /.project /.pydevproject /include /lib /lib64 /bin /virtualenv *.bak */build/* */_build/* */_build/latex/* *.class #*.png .version nosetests.xml .* !.git* !.travis* !.coveragerc /distribute_setup.py sloccount.sc *.prefs MANIFEST *.log Vagrantfile.osx.* Vagrantfile.win.*PyVirtualDisplay-3.0/.travis.yml.bak000066400000000000000000000040671420171065300175140ustar00rootroot00000000000000language: python matrix: include: - name: 3.6_trusty python: 3.6 dist: trusty - name: 3.6_xenial python: 3.6 dist: xenial - name: 3.7_xenial python: 3.7 dist: xenial - name: 3.8_xenial python: 3.8 dist: xenial - name: 3.7_bionic python: 3.7 dist: bionic - name: 3.8_bionic python: 3.8 dist: bionic - name: 3.8_focal python: 3.8 dist: focal - name: 3.9_focal python: 3.9 dist: focal - name: 3.9_focal_no_displayfd python: 3.9 dist: focal env: - PYVIRTUALDISPLAY_DISPLAYFD=0 - name: "Python 3.7 on macOS" os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS env: PATH=/Users/travis/Library/Python/3.7/bin:$PATH PIPUSER=--user # - name: "Python 3.8 on Windows" # os: windows # Windows 10.0.17134 N/A Build 17134 # language: shell # 'language: python' is an error on Travis CI Windows # before_install: # - choco install python --version 3.8 # - python -m pip install --upgrade pip # env: PATH=/c/Python38:/c/Python38/Scripts:$PATH addons: apt: packages: - xvfb - xserver-xephyr - scrot - gnumeric - x11-utils - x11-apps - xfonts-base # vnc4server is later renamed to tigervnc-standalone-server install: - if [ ${TRAVIS_OS_NAME} == "linux" ]; then sudo apt-get install -y vnc4server || true; fi - if [ ${TRAVIS_OS_NAME} == "linux" ]; then sudo apt-get install -y tigervnc-standalone-server || true; fi - PYTHON=python3 - if [ ${TRAVIS_OS_NAME} == "windows" ]; then PYTHON=python; fi - $PYTHON -m pip install $PIPUSER --upgrade -r requirements-test.txt - $PYTHON -m pip install $PIPUSER --upgrade . # http://blog.tigerteufel.de/?p=476 - if [ ${TRAVIS_OS_NAME} == "osx" ]; then mkdir /tmp/.X11-unix;sudo chmod 1777 /tmp/.X11-unix;sudo chown root /tmp/.X11-unix/; fi script: - cd tests - $PYTHON -m pytest -v . PyVirtualDisplay-3.0/LICENSE.txt000066400000000000000000000024351420171065300164670ustar00rootroot00000000000000Copyright (c) 2011, ponty All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. PyVirtualDisplay-3.0/MANIFEST.in000066400000000000000000000000551420171065300163760ustar00rootroot00000000000000include LICENSE* recursive-include tests *.pyPyVirtualDisplay-3.0/README.md000066400000000000000000000214351420171065300161240ustar00rootroot00000000000000pyvirtualdisplay is a python wrapper for [Xvfb][1], [Xephyr][2] and [Xvnc][3] Links: * home: https://github.com/ponty/pyvirtualdisplay * PYPI: https://pypi.python.org/pypi/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, 3.10 - back-ends: [Xvfb][1], [Xephyr][2] and [Xvnc][3] Possible applications: * headless run * GUI testing * automatic GUI screenshot Installation ============ install the program: ```console $ python3 -m pip install pyvirtualdisplay ``` optional: [Pillow][pillow] should be installed for ``smartdisplay`` submodule: ```console $ 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 ``` Install all dependencies and backends on Ubuntu 20.04: ```console $ 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 `start()` and `stop()` methods: ```py 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. Controlling the display with context manager: ```py from pyvirtualdisplay import Display with Display() as disp: # display is active pass # display is stopped ``` Selecting Xvfb backend: ```py disp=Display() # or disp=Display(visible=False) # or disp=Display(backend="xvfb") ``` Selecting Xephyr backend: ```py disp=Display(visible=True) # or disp=Display(backend="xephyr") ``` Selecting Xvnc backend: ```py disp=Display(backend="xvnc") ``` Setting display size: ```py disp=Display(size=(100, 60)) ``` Setting display color depth: ```py disp=Display(color_depth=24) ``` Headless run ------------ A messagebox is displayed on a hidden display. ```py # 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: ```console $ 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. ```py # 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: ```console $ python3 -m pyvirtualdisplay.examples.vncserver ``` Check it with vncviewer: ```console $ vncviewer localhost:5904 ``` ![](doc/gen/vncviewer_localhost:5904.png) GUI Test -------- ```py # 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: ```console $ python3 -m pyvirtualdisplay.examples.lowres ``` Image: ![](doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png) Screenshot ---------- ```py # 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: ```console $ python3 -m pyvirtualdisplay.examples.screenshot ``` Image: ![](doc/gen/xmessage.png) Nested Xephyr ------------- ```py # 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: ```console $ python3 -m pyvirtualdisplay.examples.nested ``` Image: ![](doc/gen/python3_-m_pyvirtualdisplay.examples.nested.png) 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. 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`][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. ```py # 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: ```console $ python3 -m pyvirtualdisplay.examples.threadsafe ``` Images: ![](doc/gen/xmessage1.png) ![](doc/gen/xmessage2.png) Hierarchy ========= ![Alt text](https://g.gravizo.com/source/svg?https%3A%2F%2Fraw.githubusercontent.com/ponty/pyvirtualdisplay/master/doc/hierarchy.dot) [1]: http://en.wikipedia.org/wiki/Xvfb [2]: http://en.wikipedia.org/wiki/Xephyr [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/EasyProcessPyVirtualDisplay-3.0/Vagrantfile000066400000000000000000000010311420171065300170200ustar00rootroot00000000000000Vagrant.configure(2) do |config| config.vm.box = "ubuntu/focal64" config.vm.provider "virtualbox" do |vb| #vb.gui = true vb.memory = "2048" 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"] end config.vm.provision "shell", path: "tests/vagrant/ubuntu2004.sh" config.ssh.extra_args = ["-t", "cd /vagrant; bash --login"] end PyVirtualDisplay-3.0/clean.py000077500000000000000000000007251420171065300163030ustar00rootroot00000000000000#!/usr/bin/env python3 import pathlib import shutil [shutil.rmtree(p) for p in pathlib.Path(".").glob(".tox")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("dist")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("*.egg-info")] [shutil.rmtree(p) for p in pathlib.Path(".").glob("build")] [p.unlink() for p in pathlib.Path(".").rglob("*.py[co]")] [p.rmdir() for p in pathlib.Path(".").rglob("__pycache__")] [p.unlink() for p in pathlib.Path(".").rglob("*.log")] PyVirtualDisplay-3.0/doc/000077500000000000000000000000001420171065300154055ustar00rootroot00000000000000PyVirtualDisplay-3.0/doc/gen/000077500000000000000000000000001420171065300161565ustar00rootroot00000000000000PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.png000066400000000000000000000362101420171065300275170ustar00rootroot00000000000000PNG  IHDR@O*<* [uἹFJ>=Hr \R[Q(.HFfG^ūBᔉڶziM9x7P2:|萕p8Ě̬zhČ0ɕ :٣s9U˖%EWz-_EQ| A{!4Az^:]z]ucqqVvζvSP`@@3!upulغMReiiyek֝8r zFk+ Qd6UaY Q:(넀o6w֌{VyAee;Ⅻ?/b~}zfo[7:!|֛~{Ё=ZIN~{SõvֶMZ }QgJVo8u' ٸ[wm7>yߟl{bd漰AVaSRVopvrߧw=/tܮt22u9Iz{{1tb}3!,=z?nG^ёb͝9#;;;-C]ϤQ9+;嫸e Z[[8;͞9ݰkfA2$$=~VYaΔElLa?*ubLȐ"Jh46͠[ ͞YD"#G#Zɉ}j0>nqؼ?%4=:tg|X,e łx\s퓒u(=jIOKPW9[ݰukg{r99ytWڍ3Lkk+bɀdkϷK׮ }/_ܿE}j8Axaڹӏdĥ➵Π Ӧu<$==G nٲk`Of•}B8:8[[ʦ-=‚djNT=x3YYrߦ5k/(mRU89:V ,dbZaVحrSvpCQ֕<) VN %sr AXjQ#B^Uq,B5T*)\۴QRRsHdmmRRR5:u`i[6ӣC gڶuk!(1hUDffjSTVkQ쪀S?+ vFإS'xU'J%F'$=A\ϫs[өԕ¢+W*-++|Zn^з™V!=֭^pي?П_]T_}%|L;aɊUI>}w͚ciE ;lLtz@t(v *1m9\΢v܅-X,<@2ױ3|A'0wַk7l=pH=wn"ܮtuBrtԈq㇎s5+~32CA?b 3 1c!3 1c!3 1cyyy| 1p@(`Č@(`Č@(`Č@(`Č!m\h2cμotsfj* 1`؈7źyWQupe-jk6ȯR٢JaOFN` :Nar*c$\3^cXwo\ժP&d=(:t?RĤY߳QkEܣ~0!mOHl'\t1\W#HӠKWa5iaص7+` 9 Y, O9'!1-] S(fRn#;޾{7x[1w}#b[3-^O8?=`a :+++"4z~*KNj߮s.NMK[r#&Zq|M[s,z񿗵.`]fgN'6dHY(^Ɍ)i/}8}tn_瞤5bY[[߽38dDbRYiր۹G/Go#'wklg›b7nŔ?.:,]!)-3sƤqc1w-]]HFj@^f?#5-m%Rys^E=}S^]y<^#50{wĢKW$ Fs.H._KYdw ~_v]{V߰n办= B 0}*c6ZLhCT>-"-JRVʇ,sy~&!f°;Dk >%¥+[[|@߾$,`cI[D?l`QZz:Օ[$ *5 l`$s'#G?إs}w&MZj?J~B9v+( BٸuJU>t@Ӻd<"7ncmmu`.[߳7Zѧl`EQ-$k~B6[I|r",8.M o߽R֭asWАad"Ś_cFS(*7UCh59%ķ\Iٺ}Gbһ!f(ݿо}Ď8={ϛ11agV[kJq8cQpe–^ߦ97c~B:iw]mD[,A 1yt;7F,[[[HgN Eǻ]><}x̋Z0dĩ'޾sL9Z8l~kX=pPww˖3҃G"LӦLnCQq++JO /}_XTT랶VDT<ӔMFQk=|z!Phr!ݻBaG+[CfvN䱟d2S\rCF:c!B==:w$nIIIjZjjzZAA\bB?;2w]zN}p6VVX@nc8P(TA0/(FZ%|q2RΜ@T(˫%is WWӬ*JTd*"'7tԙ/xlLEuT:rX*-YY(d#B[|&)\r1'7A(|?k5oH11˳f94N鴄_8V VV.Α'NN7ۋAk7sac>%eug'}4fo[7:!|֛~{Ё=ZIN~{%¢"Z-,,,޿O!]b׋ffeD"AV<`l6+-%,sϞ=KKO3 *n:'GGR|; hRS ]NsrXSVjZCc08lVdXH |?N$nBs A>uz֭yZM;g/;(=jB__ȐÑNJD"*Bl6No'l':5OM$6;,l6((&!ok9\Hsd^>R5k 2^zշouܙO(n:zYcI9m ssD"ĉ+>i哖a}ƴYlyfFu8nk7nڶ1<((ea}Wحkn]b۽{3ݸt‚ǪÒo!CPxyϧl.aht O& ΎϠ Y7;wmmli4GQѰK?=ꞝaKKKRh4A>=p a{{7OOQIiy{ {j6h-ވ.,*nB?AQνyy~B_R8áQ( g_! Tj$rlt@%\\3g4;#=5=J^|ٳg~mDbIԂɒIJ%?;vL.>Ƶfsrrs:voG?ws | o_gqqIjFL"e¶-\|ERYXX899M?޽cǎdҟ3gzpuZEg%ਨ1Fi49&,E3uXYxs箨賮666l6[!Wd)drAKKKF]^NIʪ]km P)KbXd1]=mWd%œ'NX|͟*7 8p}FoPFDRm޼K1,ww6mBCG>}ҧr4f-/Wuڵ_>ݾ«eΝ$$&h4Z۶mǍ;k}!_9ƀ*(` TjM1Z"WV ڷ@ b|_)0C/* 1c!3\ KV0_C  5P 1Pc9 ])xQ>PCi @7pq\Z*..*T!1qX0Y|PÍ (^QA^DƳ>@HJDrn4:o암k@n%@Q w v% PÍ qX$rtq`1 0 .nŢ79l8aB.csUZ8S0 n$:ymy0 0~Aqj0[E A18 #B AhPPd2pnN6ɤ̜d^r LSCG >zĉ RckQ hհ`<Ҕk<<3xcǍ:sδqgϞǍ-.(++ʴph8'A}B4a:/((pș\SL!%M-uӫW$:㩩!!!5z N0D"Ѵil޲߿\,jmJcѢEݻw;v٨(:nr T*p S*8+ `Fz5=aֽor #w̤ T* ک q}NX\^{DPctΙ]3w}.c斔m7W)N#ix͚۵kP(R сSTRԠ={",\ɓt:_#j̦+*g^?br 0$m=qAejF1U2&%&uwץ?dzgXOp BL44, 9k'?vv:t(--U* ]j5F֭ R&Ah4uww߳{WLnݾ_chOIII'Op8䄢( 8]U*ں@ ʕ wã}/a믿 ÔJ6*]Tj:,,'eeej t:yxxٽ7?{8w9 6l111Ftssr|l:fbtEєUH(*ْssu|XW{n9;q°& @Xw%V*+op|ptt$5/ZYYd/`PǎkP WP{=5 Z THʋwiGnJMI0x}|+ƠB46 HWXh\',,,j5au},}|| nz1 4{i֪cVl6BZOtϱ=HOI|~vn5w(qqmll FC26u]4V6#fԿP(D"/B1BW[l=኱Һ]x%A&m|+sKqq` 5绹 {W%̺F`sPAhaZcccccc_xŋϟ9c>|`h4=8KǏw  J޵(K=NOOܹݽ%閖3G4%kxQvvv@v}ۣш]1 NNPht9BCGDFFǷiӆ"4$!!!Ç&Ob578jxD}6IJ4{SUUr& l6i8:: Ba"jW/HǏ?%k4zunS=5'bϸNP(TTVErTd͛6,-99   4e֐)9BXEY,L&B"âV5z|>_[D9tbu E'/]daSN0L0 [VVF|}6|hZ&D7lT}۴ta3AaaZ&q0 Cxj t>}F - PTTd4痓'?ЩSɓ۷/&>9͛G שּׁ,mll\.J%ipxպƋrY_&>| >E"]|>>&QDǂa'O~add۷qq2յg wwo_Np\.ʊb5RcC7>_FCaݺ}!YY /^ܦMPo~~^QaaYYY -IDJɇ& ....\9r$dJ$}3`8B NOP*r:N3 :Qb0}8͛ 6mT*EQhSjaaamVXEĂ 5#BR-Hj`foo &Od:9:#BјLa%XXXlZM|@tE) RTWhjCr\N2Ԝ9GQ\R p\i#.[?^(@j(8;;:uٹh4*ZiB}3tz3#A[P(rd <E ([],X,ń\D"EJ݋Jm0B~ě RelP(,ֿےFfW5fʔ\4 NCZPhdV4fBsE*gA`dw.e &_B y\m r-ZYF+m]YlR%{ӛgeCnRZ-)lbsd5{5M(?G*-HP8.މkiMPWPÍ(ঀV'f ?310ToFpo9! ՕٍgUX& deeFbrPbslvz> 1-,*/{& bltKk[[AA-X&--7A-(mI}qLP̷) ]=z \.ɔH$ĤvҠ{q\VZZjjw \VƳ6+ ,**2A!$BPT|>_R+%iQeaD"1lFB3 MѴA3JҀ1TĐbzYM4SC M(`Č@(`Č7oތ ɓ'uWΝ;LL:@ ðGN6úۇkH BfLĹ[nm$ & ]{=gM^p!N3gΑ#G[!F˫rS}srrbccϟ?9s 6ilmPYP/?ա|L: ɉ&mݺCP{ 0ao޼Ν;G1s&˗/'Nhj/] O,6?=WoBݻ{V={7ߘt֭&qN+Ç뮡z"'EFF"2dFC.**"Yѱc+Wdׯ_/_ϧLRt;w4@R}@mH ʤ陔rʡC*ܷoo?>]vǏ2d@`W@ǎ[dɻwd2Y||UΝ;x-[>|XZZ:vXÇׯhтH8cfg?~gϞ;\*"<ٴi%IRRR~tYTX;sLAAA~~?lcc MJJ"ߴi㞞N:L@(>~X"~J"tu"""Ҋ>f+giG7jx;}͛zp8]t{n5$HMMZ!swwwFR9'P^ûJy>|aGtv횽}TTƍQ]p;^uڪ!ZXT*(* {n^{Ν D kH'QOOϬ,c˖t Ҳe @PYMwy{{X,??-[=žW^=qℏСåK $ ~*s .OuMԐTvljj~GGGWW׽{޺u+777i$"޽{wҥo͉fΜ٣G.dK.铫ׯ_?v옯Ӊ:n6k=z Ϟ=+**U~;9yo޺uv̘1R/^ΞyDPyܹp 9e˖ & 4R]L81...&&&55500R6y>8)UNaJ$'''m4B?w F GGǔ `|YlNT@ -[m܂T'egkjSytݻw/_~䉩}1>8\Kkۄ/\ZYE!&ɓ'\.X_*6 Lfa^NF;N 4jhmgqpv,0eD/iP!c,@$u]R,,kkFRՍge +ri SPZ-.dfeR[Ǣ|DfieKbJJJ%V~mqrA2F# A\Ka8ɤRTJ\f.]4b }F]s2>zTUx9qC 6\4 \, 0l K0i9 i4J8pUxN!b|-_? :oԨ>>z84ӯpnHA]="{\-Ι'$65\^^`0 EB4 { é4oKKw]Ȋ 81#|fʸ\nuCA (r\Tc@5z*,eddZmiiV(4K$RYuҴA`p\BT* R`1 tiT*cee U*/(u,/X,I{Y!WE3Kc&S,-I8a&ͫ%ָ̟=XrƁ#1W/|TL8uN̛CbAh#A>ɰԪKF*@/]%Ufg:pS53foRf{yy$JVP@X6gML&sЀmn^ޝZz|ٽ;if?j:'/ƭ;6YhK)'N~}U/A)ǏS7Q˾pʅKWϚ>L "v~ٙ*JgzuԙDCFt:ڦ@ĮϼvLXvLE-B{ڝ:?+=3 y۹q/yE)L?&F&0vvCLVYgemjG LZy*"BQȥLYfm[Ajuan6gYAðbo<JeL7/ bF-)!5YY?f;:qx<9ѨIN5?R&-mzIK%]^nctӧh8u, bu`R%P 1fðĤ̬l8B`N~.}@T4 Hœ @*`Nvsu5 Hœ̂X1T@ @(`Č@,S`?)Hk~oOVR͐ױvbNtl#)I璒Ns>M,BC f t䣩=@0C f 0b@C fL9ރ}@ u:~7b b_sێf@Կ10aXN^Rƍa\#jds@*.^V Lж As3>d8bZ 5С}{-T*O'Nd"݇oV&% @iL/\ `ҬX|ӂy2 ƌ:yiΘPTrǏ*umgΝ󯿯޸ecmj5|XJjj^:"(Q.D6ރBʹ]| 0,>geln':v $6 v򎁁¡#ܽqL:=(]۷SkV,uL&ӫ 7װ%Et12dH.υևױ$q=qǤqctvS m{{bd漰EG+mq;'Mk~Я?0CA>uz֭yZM;g/j>jtZ~;fYL&vh>)9׮\vr})++kiw@@/Y(nY~þ;kkKF`2.2Je(`M_G5^@lkaQοk‚R*M%IY"% !¢v;*3oEܻ'x{y]/?t>V/BNz!"C9u:*!) `.kV,#|;cwvrt2/ƥ& }Gl'ͧdGSYd 4!3 1c!3 1c!3 1c!3AH{?{IENDB`PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.lowres.txt000066400000000000000000000000561420171065300275510ustar00rootroot00000000000000$ python3 -m pyvirtualdisplay.examples.lowres PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.png000066400000000000000000000015531420171065300274700ustar00rootroot00000000000000PNG  IHDRƝ2IDATxێ@Qi_ z0S3p;N/?w]Ⱥ|`EBX$nfv7fᙗjᆴ?gks:Veblb:wDyB~&d:zX$7L۶n7g׃|:_:Nק$F |kM:G lCGބh:^ɧ+9*|%s sk$L,#,yj3gȤqXp a asm>;/ywnXsKo~-sk&&ƉaK vPja}xZ[51>6K {睄EBX$EBX$EBX$EBX$EBX$EBX$EBX$EBX$EBX$E‡3!L,"!,"!,"!,"!,"!,"!,"!,"!,"!,y`b a a a a a a a a a JW#IENDB`PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.nested.txt000066400000000000000000000000561420171065300275200ustar00rootroot00000000000000$ python3 -m pyvirtualdisplay.examples.nested PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.screenshot.txt000066400000000000000000000000621420171065300304100ustar00rootroot00000000000000$ python3 -m pyvirtualdisplay.examples.screenshot PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.threadsafe.txt000066400000000000000000000000621420171065300303410ustar00rootroot00000000000000$ python3 -m pyvirtualdisplay.examples.threadsafe PyVirtualDisplay-3.0/doc/gen/python3_-m_pyvirtualdisplay.examples.vncserver.txt000066400000000000000000000000611420171065300302470ustar00rootroot00000000000000$ python3 -m pyvirtualdisplay.examples.vncserver PyVirtualDisplay-3.0/doc/gen/vncviewer_localhost:5904.png000066400000000000000000000007051420171065300233620ustar00rootroot00000000000000PNG  IHDRC4^]IDATxY -݃YCQ:@GzY7RxV %፿*=[Mdy望n`,-?dhƞ89T&:wg] ]3ƴau3NJ)ugj25Co@X3]"#*"w0IIv}]EScc@z!hRyVn@ǒ°N٥Ln \몮KtVfOECm.w K([q6\n3:i?;AQ|Ό=^~V{Kpx9[OȎUz JiaKOP .TXOf(5D^4|Ҋ73!ύc~(o4q_IENDB`PyVirtualDisplay-3.0/doc/gen/vncviewer_localhost:5904.txt000066400000000000000000000000331420171065300234070ustar00rootroot00000000000000$ vncviewer localhost:5904 PyVirtualDisplay-3.0/doc/gen/xmessage.png000066400000000000000000000007051420171065300205020ustar00rootroot00000000000000PNG  IHDRC4^]IDATxY -݃YCQ:@GzY7RxV %፿*=[Mdy望n`,-?dhƞ89T&:wg] ]3ƴau3NJ)ugj25Co@X3]"#*"w0IIv}]EScc@z!hRyVn@ǒ°N٥Ln \몮KtVfOECm.w K([q6\n3:i?;AQ|Ό=^~V{Kpx9[OȎUz JiaKOP .TXOf(5D^4|Ҋ73!ύc~(o4q_IENDB`PyVirtualDisplay-3.0/doc/gen/xmessage1.png000066400000000000000000000005611420171065300205630ustar00rootroot00000000000000PNG  IHDR.4z8IDATxW &3@ [=g}pP(́ϯ \XT4eyrrgx#cFߠc¡P9kSnD7EEDT_#vT3(@;a$Yү.xvU ctU7*$QqUIуx~/-W{V39{:!-pTqY7o9~*E6"ۨfN]akbsվ^qȩz62HұDDQd. 5Wxoq15{Qk`IENDB`PyVirtualDisplay-3.0/doc/gen/xmessage2.png000066400000000000000000000006071420171065300205650ustar00rootroot00000000000000PNG  IHDR.4zNIDATxW N!dNMQ"= wb~Ԡ XvfbDisplay; Display -> XephyrDisplay; Display -> XvncDisplay; SmartDisplay -> Display } XvfbDisplay -> Xvfb; XephyrDisplay -> Xephyr; XvncDisplay -> Xvnc; application -> Display; application -> SmartDisplay; SmartDisplay -> Pillow; } PyVirtualDisplay-3.0/format-code.sh000077500000000000000000000001771420171065300174040ustar00rootroot00000000000000#!/bin/bash set -e autoflake -i -r --remove-all-unused-imports . autoflake -i -r --remove-unused-variables . isort . black . PyVirtualDisplay-3.0/lint.sh000077500000000000000000000002431420171065300161440ustar00rootroot00000000000000#!/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" PyVirtualDisplay-3.0/pytest.ini000066400000000000000000000002161420171065300166700ustar00rootroot00000000000000[pytest] log_level=DEBUG log_format=%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s log_date_format=%Y-%m-%d %H:%M:%S #log_cli=truePyVirtualDisplay-3.0/pyvirtualdisplay/000077500000000000000000000000001420171065300202655ustar00rootroot00000000000000PyVirtualDisplay-3.0/pyvirtualdisplay/__init__.py000066400000000000000000000003611420171065300223760ustar00rootroot00000000000000import logging from pyvirtualdisplay.about import __version__ from pyvirtualdisplay.display import Display Display # ignore unused log = logging.getLogger(__name__) log = logging.getLogger(__name__) log.debug("version=%s", __version__) PyVirtualDisplay-3.0/pyvirtualdisplay/about.py000066400000000000000000000000241420171065300217450ustar00rootroot00000000000000__version__ = "3.0" PyVirtualDisplay-3.0/pyvirtualdisplay/abstractdisplay.py000066400000000000000000000304471420171065300240400ustar00rootroot00000000000000import fnmatch import logging import os import select import subprocess import tempfile import time from threading import Lock from pyvirtualdisplay import xauth from pyvirtualdisplay.util import get_helptext, platform_is_osx log = logging.getLogger(__name__) # try: # import fcntl # except ImportError: # fcntl = None _mutex = Lock() _mutex_popen = Lock() _MIN_DISPLAY_NR = 1000 _USED_DISPLAY_NR_LIST = [] _X_START_TIMEOUT = 10 _X_START_TIME_STEP = 0.1 _X_START_WAIT = 0.1 class XStartTimeoutError(Exception): pass class XStartError(Exception): pass def _lock_files(): tmpdir = "/tmp" try: ls = os.listdir(tmpdir) except FileNotFoundError: log.warning("missing /tmp") return [] pattern = ".X*-lock" names = fnmatch.filter(ls, pattern) ls = [os.path.join(tmpdir, child) for child in names] ls = [p for p in ls if os.path.isfile(p)] return ls def _search_for_display(): # search for free display ls = list(map(lambda x: int(x.split("X")[1].split("-")[0]), _lock_files())) if len(ls): display = max(_MIN_DISPLAY_NR, max(ls) + 3) else: display = _MIN_DISPLAY_NR return display class AbstractDisplay(object): """ Common parent for X servers (Xvfb,Xephyr,Xvnc) """ def __init__(self, program, use_xauth, retries, extra_args, manage_global_env): self._extra_args = extra_args self._retries = retries self._program = program self.stdout = None self.stderr = None self.old_display_var = None self._subproc = None self.display = None self._is_started = False self._manage_global_env = manage_global_env self._reset_global_env = False self._pipe_wfd = None self._retries_current = 0 helptext = get_helptext(program) self._has_displayfd = "-displayfd" in helptext if not self._has_displayfd: log.debug("-displayfd flag is missing.") PYVIRTUALDISPLAY_DISPLAYFD = os.environ.get("PYVIRTUALDISPLAY_DISPLAYFD") if PYVIRTUALDISPLAY_DISPLAYFD: log.debug("PYVIRTUALDISPLAY_DISPLAYFD=%s", PYVIRTUALDISPLAY_DISPLAYFD) # '0'->false, '1'->true self._has_displayfd = bool(int(PYVIRTUALDISPLAY_DISPLAYFD)) else: # TODO: macos: displayfd is available on XQuartz-2.7.11 but it doesn't work, always 0 is returned if platform_is_osx(): self._has_displayfd = False self._check_flags(helptext) if use_xauth and not xauth.is_installed(): raise xauth.NotFoundError() self._use_xauth = use_xauth self._old_xauth = None self._xauth_filename = None def _check_flags(self, helptext): pass def _cmd(self): raise NotImplementedError() def _redirect_display(self, on): """ on: * True -> set $DISPLAY to virtual screen * False -> set $DISPLAY to original screen :param on: bool """ d = self.new_display_var if on else self.old_display_var if d is None: log.debug("unset $DISPLAY") try: del os.environ["DISPLAY"] except KeyError: log.warning("$DISPLAY was already unset.") else: log.debug("set $DISPLAY=%s", d) os.environ["DISPLAY"] = d def _env(self): env = os.environ.copy() env["DISPLAY"] = self.new_display_var return env def start(self): """ start display :rtype: self """ if self._is_started: raise XStartError(self, "Display was started twice.") self._is_started = True if self._has_displayfd: self._start1_has_displayfd() else: i = 0 while True: self._retries_current = i + 1 try: self._start1() break except XStartError: log.warning("start failed %s", i + 1) time.sleep(0.05) i += 1 if i >= self._retries: raise XStartError( "No success after %s retries. Last stderr: %s" % (self._retries, self.stderr) ) if self._manage_global_env: self._redirect_display(True) self._reset_global_env = True def _popen(self, use_pass_fds): with _mutex_popen: if use_pass_fds: self._subproc = subprocess.Popen( self._command, pass_fds=[self._pipe_wfd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, ) else: self._subproc = subprocess.Popen( self._command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, ) def _start1_has_displayfd(self): # stdout doesn't work on osx -> create own pipe rfd, self._pipe_wfd = os.pipe() self._command = self._cmd() + self._extra_args log.debug("command: %s", self._command) self._popen(use_pass_fds=True) self.display = int(self._wait_for_pipe_text(rfd)) os.close(rfd) os.close(self._pipe_wfd) self.new_display_var = ":%s" % int(self.display) if self._use_xauth: self._setup_xauth() # https://github.com/ponty/PyVirtualDisplay/issues/2 # https://github.com/ponty/PyVirtualDisplay/issues/14 self.old_display_var = os.environ.get("DISPLAY", None) def _start1(self): with _mutex: self.display = _search_for_display() while self.display in _USED_DISPLAY_NR_LIST: self.display += 1 self.new_display_var = ":%s" % int(self.display) _USED_DISPLAY_NR_LIST.append(self.display) self._command = self._cmd() + self._extra_args log.debug("command: %s", self._command) self._popen(use_pass_fds=False) self.new_display_var = ":%s" % int(self.display) if self._use_xauth: self._setup_xauth() # https://github.com/ponty/PyVirtualDisplay/issues/2 # https://github.com/ponty/PyVirtualDisplay/issues/14 self.old_display_var = os.environ.get("DISPLAY", None) # wait until X server is active start_time = time.time() d = self.new_display_var ok = False time.sleep(0.05) # give time for early exit while True: if not self.is_alive(): break try: 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 else: log.info('Successfully started X with display "%s".', d) ok = True break if time.time() - start_time >= _X_START_TIMEOUT: break time.sleep(_X_START_TIME_STEP) if not self.is_alive(): log.warning("process exited early. stderr:%s", self.stderr) msg = "Failed to start process: %s" raise XStartError(msg % self) if not ok: msg = 'Failed to start X on display "%s" (xdpyinfo check failed, stderr:[%s]).' raise XStartTimeoutError(msg % (d, xdpyinfo.stderr)) def _wait_for_pipe_text(self, rfd): s = "" start_time = time.time() while True: (rfd_changed_ls, _, _) = select.select([rfd], [], [], 0.1) if not self.is_alive(): raise XStartError( "%s program closed. command: %s stderr: %s" % (self._program, self._command, self.stderr) ) if rfd in rfd_changed_ls: c = os.read(rfd, 1) if c == b"\n": break s += c.decode("ascii") # 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): """ stop display :rtype: self """ if not self._is_started: raise XStartError("stop() is called before start().") if self._reset_global_env: self._redirect_display(False) if self.is_alive(): try: self._subproc.kill() except OSError as oserror: log.debug("exception in terminate:%s", oserror) self._subproc.wait() self._read_stdout_stderr() if self._use_xauth: self._clear_xauth() return self def _read_stdout_stderr(self): if self.stdout is None: (self.stdout, self.stderr) = self._subproc.communicate() log.debug("stdout=%s", self.stdout) log.debug("stderr=%s", self.stderr) def _setup_xauth(self): """ Set up the Xauthority file and the XAUTHORITY environment variable. """ handle, filename = tempfile.mkstemp( prefix="PyVirtualDisplay.", suffix=".Xauthority" ) self._xauth_filename = filename os.close(handle) # Save old environment self._old_xauth = {} self._old_xauth["AUTHFILE"] = os.getenv("AUTHFILE") self._old_xauth["XAUTHORITY"] = os.getenv("XAUTHORITY") os.environ["AUTHFILE"] = os.environ["XAUTHORITY"] = filename cookie = xauth.generate_mcookie() xauth.call("add", self.new_display_var, ".", cookie) def _clear_xauth(self): """ Clear the Xauthority file and restore the environment variables. """ os.remove(self._xauth_filename) for varname in ["AUTHFILE", "XAUTHORITY"]: if self._old_xauth[varname] is None: del os.environ[varname] else: os.environ[varname] = self._old_xauth[varname] self._old_xauth = None def __enter__(self): """used by the :keyword:`with` statement""" self.start() return self def __exit__(self, *exc_info): """used by the :keyword:`with` statement""" self.stop() def is_alive(self): if not self._subproc: return False # return self.return_code is None rc = self._subproc.poll() if rc is not None: # proc exited self._read_stdout_stderr() 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): """ PID (:attr:`subprocess.Popen.pid`) :rtype: int """ if self._subproc: return self._subproc.pid PyVirtualDisplay-3.0/pyvirtualdisplay/display.py000066400000000000000000000062711420171065300223120ustar00rootroot00000000000000from typing import Dict, List, Optional, Tuple from pyvirtualdisplay.xephyr import XephyrDisplay from pyvirtualdisplay.xvfb import XvfbDisplay from pyvirtualdisplay.xvnc import XvncDisplay _class_map = {"xvfb": XvfbDisplay, "xvnc": XvncDisplay, "xephyr": XephyrDisplay} class Display(object): """ Proxy class :param color_depth: [8, 16, 24, 32] :param size: screen size (width,height) :param bgcolor: background color ['black' or 'white'] :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 which is not thread-safe. Use False to make it thread-safe. """ def __init__( self, backend: Optional[str] = None, visible: bool = False, size: Tuple[int, int] = (1024, 768), color_depth: int = 24, bgcolor: str = "black", use_xauth: bool = False, # check_startup=False, retries: int = 10, extra_args: List[str] = [], manage_global_env: bool = True, **kwargs ): self._color_depth = color_depth self._size = size self._bgcolor = bgcolor self._visible = visible self._backend = backend if not self._backend: if self._visible: self._backend = "xephyr" else: self._backend = "xvfb" cls = _class_map.get(self._backend) if not cls: raise ValueError("unknown backend: %s" % self._backend) self._obj = cls( size=size, color_depth=color_depth, bgcolor=bgcolor, retries=retries, use_xauth=use_xauth, # check_startup=check_startup, extra_args=extra_args, manage_global_env=manage_global_env, **kwargs ) def start(self) -> "Display": """ start display :rtype: self """ self._obj.start() return self def stop(self) -> "Display": """ stop display :rtype: self """ self._obj.stop() return self def __enter__(self): """used by the :keyword:`with` statement""" self.start() return self def __exit__(self, *exc_info): """used by the :keyword:`with` statement""" self.stop() def is_alive(self) -> bool: return self._obj.is_alive() # @property # def return_code(self): # return self._obj.return_code @property def pid(self) -> int: """ PID (:attr:`subprocess.Popen.pid`) :rtype: int """ return self._obj.pid @property def display(self) -> int: """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'""" return self._obj.new_display_var def env(self) -> Dict[str, str]: """env() copies global os.environ and adds disp.new_display_var""" return self._obj._env() PyVirtualDisplay-3.0/pyvirtualdisplay/examples/000077500000000000000000000000001420171065300221035ustar00rootroot00000000000000PyVirtualDisplay-3.0/pyvirtualdisplay/examples/__init__.py000066400000000000000000000000001420171065300242020ustar00rootroot00000000000000PyVirtualDisplay-3.0/pyvirtualdisplay/examples/headless.py000066400000000000000000000003651420171065300242510ustar00rootroot00000000000000"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() PyVirtualDisplay-3.0/pyvirtualdisplay/examples/lowres.py000066400000000000000000000004121420171065300237650ustar00rootroot00000000000000"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() PyVirtualDisplay-3.0/pyvirtualdisplay/examples/nested.py000066400000000000000000000013401420171065300237350ustar00rootroot00000000000000"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() PyVirtualDisplay-3.0/pyvirtualdisplay/examples/screenshot.py000066400000000000000000000011151420171065300246300ustar00rootroot00000000000000"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") PyVirtualDisplay-3.0/pyvirtualdisplay/examples/threadsafe.py000066400000000000000000000014041420171065300245620ustar00rootroot00000000000000"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() PyVirtualDisplay-3.0/pyvirtualdisplay/examples/vncserver.py000066400000000000000000000004341420171065300244730ustar00rootroot00000000000000"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() PyVirtualDisplay-3.0/pyvirtualdisplay/py.typed000066400000000000000000000000001420171065300217520ustar00rootroot00000000000000PyVirtualDisplay-3.0/pyvirtualdisplay/smartdisplay.py000066400000000000000000000051471420171065300233620ustar00rootroot00000000000000import logging import time from PIL import Image, ImageChops from PIL.ImageGrab import grab from pyvirtualdisplay import Display log = logging.getLogger(__name__) class DisplayTimeoutError(Exception): pass 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. """ if im.mode != "RGB": im = im.convert("RGB") bg = Image.new("RGB", im.size, bgcolor) diff = ImageChops.difference(im, bg) bbox = diff.getbbox() if bbox: return im.crop(bbox) return None # no contents class SmartDisplay(Display): def autocrop(self, im): """Crop borders off an image. :param im: Source image. :return: An image without borders, or None if there's no actual content in the image. """ return autocrop(im, self._bgcolor) def grab(self, autocrop=True): # TODO: use Xvfb fbdir option for screenshot img = grab(xdisplay=self.new_display_var) if autocrop: img = self.autocrop(img) return img def waitgrab(self, timeout=60, autocrop=True, cb_imgcheck=None): """start process and create screenshot. Repeat screenshot until it is not empty and cb_imgcheck callback function returns True for current screenshot. :param autocrop: True -> crop screenshot :param timeout: int :param cb_imgcheck: None or callback for testing img, True = accept img, False = reject img """ t = 0 sleep_time = 0.3 # for fast windows repeat_time = 0.5 while 1: log.debug("sleeping %s secs" % str(sleep_time)) time.sleep(sleep_time) t += sleep_time img = self.grab(autocrop=False) img_crop = self.autocrop(img) if autocrop: img = img_crop if img_crop: if not cb_imgcheck: break if cb_imgcheck(img): break sleep_time = repeat_time repeat_time += 0.5 # progressive if t > timeout: msg = "Timeout! elapsed time:%s timeout:%s " % (t, timeout) raise DisplayTimeoutError(msg) # break log.debug("screenshot is empty, next try..") assert img # if not img: # log.debug('screenshot is empty!') return img PyVirtualDisplay-3.0/pyvirtualdisplay/util.py000066400000000000000000000007461420171065300216230ustar00rootroot00000000000000import subprocess import sys def get_helptext(program): 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 def platform_is_osx(): return sys.platform == "darwin" PyVirtualDisplay-3.0/pyvirtualdisplay/xauth.py000066400000000000000000000022171420171065300217720ustar00rootroot00000000000000"""Utility functions for xauth.""" import hashlib import os import subprocess class NotFoundError(Exception): """Error when xauth was not found.""" def is_installed(): """ Return whether or not xauth is installed. """ try: 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 def generate_mcookie(): """ Generate a cookie string suitable for xauth. """ data = os.urandom(16) # 16 bytes = 128 bit return hashlib.md5(data).hexdigest() def call(*args): """ Call xauth with the given args. """ xauth = subprocess.Popen( ["xauth"] + list(args), # env=self._env(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _, _ = xauth.communicate() # EasyProcess(["xauth"] + list(args)).call() PyVirtualDisplay-3.0/pyvirtualdisplay/xephyr.py000066400000000000000000000031431420171065300221570ustar00rootroot00000000000000import logging from pyvirtualdisplay.abstractdisplay import AbstractDisplay log = logging.getLogger(__name__) PROGRAM = "Xephyr" class XephyrDisplay(AbstractDisplay): """ Xephyr wrapper Xephyr is an X server outputting to a window on a pre-existing X display """ def __init__( self, size=(1024, 768), color_depth=24, bgcolor="black", use_xauth=False, retries=10, extra_args=[], manage_global_env=True, parent=None, ): """ :param bgcolor: 'black' or 'white' """ self._color_depth = color_depth self._size = size self._bgcolor = bgcolor self._parent = parent AbstractDisplay.__init__( self, PROGRAM, use_xauth=use_xauth, retries=retries, extra_args=extra_args, manage_global_env=manage_global_env, ) def _check_flags(self, helptext): 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])), ] ) if self._has_displayfd: cmd += ["-displayfd", str(self._pipe_wfd)] else: cmd += [self.new_display_var] if self._has_resizeable: cmd += ["-resizeable"] return cmd PyVirtualDisplay-3.0/pyvirtualdisplay/xvfb.py000066400000000000000000000036251420171065300216120ustar00rootroot00000000000000import logging from pyvirtualdisplay.abstractdisplay import AbstractDisplay log = logging.getLogger(__name__) PROGRAM = "Xvfb" class XvfbDisplay(AbstractDisplay): """ Xvfb wrapper Xvfb is an X server that can run on machines with no display hardware and no physical input devices. It emulates a dumb framebuffer using virtual memory. """ def __init__( self, size=(1024, 768), color_depth=24, bgcolor="black", use_xauth=False, fbdir=None, dpi=None, retries=10, extra_args=[], manage_global_env=True, ): """ :param bgcolor: 'black' or 'white' :param fbdir: If non-null, the virtual screen is memory-mapped to a file in the given directory ('-fbdir' option) :param dpi: screen resolution in dots per inch if not None """ self._screen = 0 self._size = size self._color_depth = color_depth self._bgcolor = bgcolor self._fbdir = fbdir self._dpi = dpi AbstractDisplay.__init__( self, PROGRAM, use_xauth=use_xauth, retries=retries, extra_args=extra_args, manage_global_env=manage_global_env, ) def _check_flags(self, helptext): pass def _cmd(self): cmd = [ dict(black="-br", white="-wr")[self._bgcolor], "-nolisten", "tcp", "-screen", str(self._screen), "x".join(map(str, list(self._size) + [self._color_depth])), ] if self._fbdir: cmd += ["-fbdir", self._fbdir] if self._dpi is not None: cmd += ["-dpi", str(self._dpi)] if self._has_displayfd: cmd += ["-displayfd", str(self._pipe_wfd)] else: cmd += [self.new_display_var] return [PROGRAM] + cmd PyVirtualDisplay-3.0/pyvirtualdisplay/xvnc.py000066400000000000000000000035751420171065300216270ustar00rootroot00000000000000import logging from pyvirtualdisplay.abstractdisplay import AbstractDisplay log = logging.getLogger(__name__) PROGRAM = "Xvnc" class XvncDisplay(AbstractDisplay): """ Xvnc wrapper """ def __init__( self, size=(1024, 768), color_depth=24, bgcolor="black", use_xauth=False, rfbport=5900, rfbauth=None, retries=10, extra_args=[], manage_global_env=True, ): """ :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 rfbauth: Specifies the file containing the password used to authenticate viewers. """ self._size = size self._color_depth = color_depth self._bgcolor = bgcolor self._rfbport = rfbport self._rfbauth = rfbauth AbstractDisplay.__init__( self, PROGRAM, use_xauth=use_xauth, retries=retries, extra_args=extra_args, manage_global_env=manage_global_env, ) def _check_flags(self, helptext): pass def _cmd(self): cmd = [ PROGRAM, "-depth", str(self._color_depth), "-geometry", "%dx%d" % (self._size[0], self._size[1]), "-rfbport", str(self._rfbport), ] if self._rfbauth: cmd += ["-rfbauth", str(self._rfbauth)] # default: # -SecurityTypes = VncAuth else: cmd += ["-SecurityTypes", "None"] if self._has_displayfd: cmd += ["-displayfd", str(self._pipe_wfd)] else: cmd += [self.new_display_var] return cmd PyVirtualDisplay-3.0/requirements-doc.txt000066400000000000000000000002051420171065300206640ustar00rootroot00000000000000autoflake isort black # pytest pillow entrypoint2 vncdotool==0.13.0 # psutil # for travis xenial # attrs # pytest-xdist EasyProcess PyVirtualDisplay-3.0/requirements-test.txt000066400000000000000000000001771420171065300211060ustar00rootroot00000000000000pytest pillow types-pillow entrypoint2 vncdotool==0.13.0 psutil # for travis xenial attrs pytest-xdist mypy flake8 EasyProcess PyVirtualDisplay-3.0/setup.py000066400000000000000000000030371420171065300163550ustar00rootroot00000000000000import os.path from setuptools import setup NAME = "pyvirtualdisplay" # get __version__ __version__ = None exec(open(os.path.join(NAME, "about.py")).read()) VERSION = __version__ PYPI_NAME = "PyVirtualDisplay" URL = "https://github.com/ponty/pyvirtualdisplay" DESCRIPTION = "python wrapper for Xvfb, Xephyr and Xvnc" LONG_DESCRIPTION = """pyvirtualdisplay is a python wrapper for Xvfb, Xephyr and Xvnc Documentation: https://github.com/ponty/pyvirtualdisplay/tree/""" LONG_DESCRIPTION += VERSION PACKAGES = [ NAME, NAME + ".examples", ] classifiers = [ # Get more strings from # http://www.python.org/pypi?%3Aaction=list_classifiers "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] setup( name=PYPI_NAME, version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type="text/x-rst", classifiers=classifiers, keywords="Xvfb Xephyr X wrapper", author="ponty", # author_email='', url=URL, license="BSD", packages=PACKAGES, # install_requires=install_requires, package_data={ NAME: ["py.typed"], }, ) PyVirtualDisplay-3.0/tests/000077500000000000000000000000001420171065300160025ustar00rootroot00000000000000PyVirtualDisplay-3.0/tests/slowgui.py000066400000000000000000000002511420171065300200430ustar00rootroot00000000000000import time from easyprocess import EasyProcess def main(): time.sleep(10) EasyProcess(["xmessage", "hello"]).start() if __name__ == "__main__": main() PyVirtualDisplay-3.0/tests/test_core.py000066400000000000000000000123301420171065300203420ustar00rootroot00000000000000from 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 def test_virt(): vd = Display() # assert vd.return_code is None assert not vd.is_alive() vd.start() # assert vd.return_code is None assert vd.is_alive() vd.stop() # assert vd.return_code == 0 assert not vd.is_alive() vd = Display().start().stop() # assert vd.return_code == 0 assert not vd.is_alive() def test_nest(): vd = Display().start() assert vd.is_alive() nd = Display(visible=True).start().stop() # assert nd.return_code == 0 assert not nd.is_alive() vd.stop() assert not vd.is_alive() def test_disp(): vd = Display().start() assert vd.is_alive() # d = Display(visible=True).start().sleep(2).stop() # .assertEquals(d.return_code, 0) d = Display(visible=False).start().stop() # assert d.return_code == 0 assert not d.is_alive() vd.stop() assert not vd.is_alive() def test_repr_xvfb(): display = Display() print(repr(display)) display = Display(visible=False) print(repr(display)) display = Display(backend="xvfb") print(repr(display)) display = XvfbDisplay() print(repr(display)) if has_xvnc(): def test_repr_xvnc(): display = Display(backend="xvnc", rfbport=rfbport()) print(repr(display)) display = XvncDisplay() print(repr(display)) def test_repr_xephyr(): display = Display(visible=True) print(repr(display)) display = Display(backend="xephyr") print(repr(display)) display = XephyrDisplay() print(repr(display)) def test_stop_nostart(): with pytest.raises(XStartError): Display().stop() def test_double_start(): vd = Display() try: vd.start() with pytest.raises(XStartError): vd.start() finally: vd.stop() def test_double_stop(): vd = Display().start().stop() # assert vd.return_code == 0 assert not vd.is_alive() vd.stop() # assert vd.return_code == 0 assert not vd.is_alive() def test_stop_terminated(): vd = Display().start() assert vd.is_alive() vd._obj._subproc.kill() sleep(1) assert not vd.is_alive() vd.stop() # assert vd.return_code == 0 assert not vd.is_alive() def test_no_backend(): with pytest.raises(ValueError): Display(backend="unknown") def test_color_xvfb(): with pytest.raises(XStartError): Display(color_depth=99).start().stop() Display(color_depth=16).start().stop() Display(color_depth=24).start().stop() Display(color_depth=8).start().stop() def test_color_xephyr(): with Display(): # requested screen depth not supported, setting to match hosts Display(backend="xephyr", color_depth=99).start().stop() Display(backend="xephyr", color_depth=16).start().stop() Display(backend="xephyr", color_depth=24).start().stop() Display(backend="xephyr", color_depth=8).start().stop() if has_xvnc(): def test_color_xvnc(): with pytest.raises(XStartError): with Display(backend="xvnc", color_depth=99, rfbport=rfbport()): pass with Display(backend="xvnc", color_depth=16, rfbport=rfbport()): pass with Display(backend="xvnc", color_depth=24, rfbport=rfbport()): pass # tigervnc no longer works 8-bit pseudocolors, 18.04 is OK # with Display(backend="xvnc", color_depth=8, rfbport=rfbport()): # pass def test_pid(): with Display() as d: assert d.pid > 0 with XvfbDisplay() as d: assert d.pid > 0 def test_bgcolor(): Display(bgcolor="black").start().stop() Display(bgcolor="white").start().stop() with pytest.raises(KeyError): Display(bgcolor="green").start().stop() def test_is_started(): # d = Display() # assert not d._is_started # d.start() # assert d._is_started # d.stop() # assert d._is_started # with Display() as d: # assert d._is_started # assert d._is_started with XvfbDisplay() as d: assert d._is_started assert d._is_started with Display(): with XephyrDisplay() as d: assert d._is_started assert d._is_started # with XvncDisplay() as d: # assert d._is_started # assert d._is_started def test_extra_args(): # Unrecognized option d = Display(extra_args=["willcrash"]) with pytest.raises(XStartError): d.start() with Display(): # -c turns off key-click with Display(visible=True, extra_args=["-c"]) as d: assert d.is_alive() assert not d.is_alive() with XephyrDisplay(extra_args=["-c"]) as d: assert d.is_alive() assert not d.is_alive() def test_display(): d = Display() assert d.display is None d.start() assert d.display >= 0 d = XvfbDisplay() assert d.display is None d.start() assert d.display >= 0 PyVirtualDisplay-3.0/tests/test_examples.py000066400000000000000000000031441420171065300212330ustar00rootroot00000000000000import logging import os import sys 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 log = logging.getLogger(__name__) python = sys.executable def test_screenshot(): owd = os.getcwd() with TemporaryDirectory(prefix="pyvirtualdisplay_") as tmpdirname: try: os.chdir(tmpdirname) p = EasyProcess([python, "-m", "pyvirtualdisplay.examples.screenshot"]) p.call() assert p.return_code == 0 finally: os.chdir(owd) def test_headless(): p = EasyProcess([python, "-m", "pyvirtualdisplay.examples.headless"]).start() sleep(1) assert p.is_alive() # p.stop() kill_process_tree(p) def test_nested(): with Display(): p = EasyProcess([python, "-m", "pyvirtualdisplay.examples.nested"]).start() sleep(1) assert p.is_alive() # p.stop() kill_process_tree(p) if has_xvnc(): def test_vncserver(): if worker() == 0: p = EasyProcess( [python, "-m", "pyvirtualdisplay.examples.vncserver"] ).start() sleep(1) assert p.is_alive() # p.stop() kill_process_tree(p) if prog_check(["gnumeric", "-help"]): def test_lowres(): with Display(): p = EasyProcess([python, "-m", "pyvirtualdisplay.examples.lowres"]).start() sleep(1) assert p.is_alive() # p.stop() kill_process_tree(p) PyVirtualDisplay-3.0/tests/test_race.py000066400000000000000000000040041420171065300203230ustar00rootroot00000000000000import sys from time import sleep from easyprocess import EasyProcess from entrypoint2 import entrypoint from tutil import has_xvnc, worker from pyvirtualdisplay import Display # ubuntu 14.04 no displayfd # ubuntu 16.04 displayfd # Xephyr leaks shared memory segments until 18.04 # https://gitlab.freedesktop.org/xorg/xserver/-/issues/130 # approximately 4MB/call def test_race_100_xvfb(): check_n(100, "xvfb") # TODO: this fails with "Xephyr cannot open host display" # Xephyr bug? # def test_race_500_100_xephyr(): # for _ in range(500): # check_n(100, "xephyr") if has_xvnc(): def test_race_10_xvnc(): check_n(10, "xvnc") def check_n(n, backend): with Display(): ls = [] try: for i in range(n): cmd = [ sys.executable, __file__.rsplit(".", 1)[0] + ".py", str(i), backend, str(n), "--debug", ] p = EasyProcess(cmd) p.start() ls += [p] sleep(3) good_count = 0 rc_ls = [] for p in ls: p.wait() if p.return_code == 0: good_count += 1 rc_ls += [p.return_code] finally: for p in ls: p.stop() print(rc_ls) print(good_count) assert good_count == n @entrypoint def main(i, backend, retries): retries = int(retries) kwargs = dict() if backend == "xvnc": kwargs["rfbport"] = 42000 + 100 * worker() + int(i) # print("$DISPLAY=%s" % (os.environ.get("DISPLAY"))) 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, ) ) ok = d.is_alive() d.stop() assert ok PyVirtualDisplay-3.0/tests/test_smart.py000066400000000000000000000044001420171065300205370ustar00rootroot00000000000000import sys from pathlib import Path import pytest from easyprocess import EasyProcess from pyvirtualdisplay import Display from pyvirtualdisplay.smartdisplay import DisplayTimeoutError, SmartDisplay python = sys.executable def test_disp(): with Display(): d = SmartDisplay(visible=True).start().stop() # assert d.return_code == 0 assert not d.is_alive() d = SmartDisplay(visible=False).start().stop() # assert d.return_code == 0 assert not d.is_alive() def test_slowshot(): disp = SmartDisplay(visible=False).start() py = Path(__file__).parent / ("slowgui.py") proc = EasyProcess([python, py]).start() img = disp.waitgrab() proc.stop() disp.stop() assert img is not None def test_slowshot_with(): disp = SmartDisplay(visible=False) py = Path(__file__).parent / ("slowgui.py") proc = EasyProcess([python, py]) with disp: with proc: img = disp.waitgrab() assert img is not None def test_slowshot_nocrop(): disp = SmartDisplay(visible=False) py = Path(__file__).parent / ("slowgui.py") proc = EasyProcess([python, py]) with disp: with proc: img = disp.waitgrab(autocrop=False) assert img is not None def test_empty(): disp = SmartDisplay(visible=False) proc = EasyProcess([python]) with disp: with proc: with pytest.raises(Exception): disp.waitgrab(timeout=10) def test_empty_nocrop(): disp = SmartDisplay(visible=False) proc = EasyProcess([python]) with disp: with proc: with pytest.raises(Exception): disp.waitgrab(autocrop=False, timeout=10) def test_slowshot_timeout(): disp = SmartDisplay(visible=False) py = Path(__file__).parent / ("slowgui.py") proc = EasyProcess([python, py]) with disp: with proc: with pytest.raises(DisplayTimeoutError): disp.waitgrab(timeout=1) def test_slowshot_timeout_nocrop(): disp = SmartDisplay(visible=False) py = Path(__file__).parent / ("slowgui.py") proc = EasyProcess([python, py]) with disp: with proc: with pytest.raises(DisplayTimeoutError): disp.waitgrab(timeout=1, autocrop=False) PyVirtualDisplay-3.0/tests/test_smart2.py000066400000000000000000000007361420171065300206310ustar00rootroot00000000000000from easyprocess import EasyProcess from pyvirtualdisplay.smartdisplay import SmartDisplay def test_double(): with SmartDisplay(visible=False, bgcolor="black") as disp: with EasyProcess(["xmessage", "hello1"]): img = disp.waitgrab() assert img is not None with SmartDisplay(visible=False, bgcolor="black") as disp: with EasyProcess(["xmessage", "hello2"]): img = disp.waitgrab() assert img is not None PyVirtualDisplay-3.0/tests/test_smart_thread.py000066400000000000000000000022261420171065300220720ustar00rootroot00000000000000import logging from threading import Thread from time import sleep from easyprocess import EasyProcess from PIL import ImageChops from pyvirtualdisplay.smartdisplay import SmartDisplay def func2(results): with SmartDisplay(manage_global_env=False) as disp: env = disp.env() logging.info("env=%s", env) sleep(2) with EasyProcess(["xmessage", "hello"], env=env): im = disp.waitgrab(timeout=1) results[0] = im sleep(2) def test_smart(): results = [None] t = Thread(target=func2, args=(results,)) t.start() sleep(1) with SmartDisplay(manage_global_env=False) as disp: env = disp.env() logging.info("env=%s", env) with EasyProcess(["xmessage", "hello"], env=env): sleep(2) im0 = disp.waitgrab(timeout=1) assert im0 t.join() im1 = results[0] assert im1 # im0.save('/vagrant/im0.png') # im1.save('/vagrant/im1.png') img_diff = ImageChops.difference(im0, im1) ex = img_diff.getextrema() logging.debug("diff getextrema: %s", ex) diff_bbox = img_diff.getbbox() assert diff_bbox is None PyVirtualDisplay-3.0/tests/test_threads.py000066400000000000000000000017021420171065300210450ustar00rootroot00000000000000from threading import Lock, Thread from time import sleep from easyprocess import EasyProcess from pyvirtualdisplay import Display disps = [] mutex = Lock() def get_display(threadid, disp): dispnr = EasyProcess(["sh", "-c", "echo $DISPLAY"], env=disp.env()).call().stdout with mutex: disps.append((threadid, dispnr)) def func(): with Display(manage_global_env=False) as disp: get_display(1, disp) sleep(2) get_display(1, disp) def test_disp_var(): t = Thread(target=func) t.start() sleep(1) with Display(manage_global_env=False) as disp: get_display(0, disp) sleep(2) get_display(0, disp) t.join() print(disps) assert disps[0][0] == 1 assert disps[1][0] == 0 assert disps[2][0] == 1 assert disps[3][0] == 0 # :1 assert disps[0][1] == disps[2][1] # :0 assert disps[1][1] == disps[3][1] assert disps[0][1] != disps[1][1] PyVirtualDisplay-3.0/tests/test_with.py000066400000000000000000000030131420171065300203630ustar00rootroot00000000000000from 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 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 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 not vd.is_alive() def test_with_xephyr(): with Display() as vd: with Display(visible=True, size=(800, 600)) as vd: assert vd.is_alive() assert vd._backend == "xephyr" # 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 not vd.is_alive() if has_xvnc(): def test_with_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 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 not vd.is_alive() PyVirtualDisplay-3.0/tests/test_xauth.py000066400000000000000000000014441420171065300205470ustar00rootroot00000000000000import 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. """ 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") display.stop() assert old_xauth == os.getenv("XAUTHORITY") assert not os.path.isfile(new_xauth) PyVirtualDisplay-3.0/tests/test_xvnc.py000066400000000000000000000035751420171065300204030ustar00rootroot00000000000000import 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 if has_xvnc(): def test_xvnc(): with tempfile.TemporaryDirectory() as temp_dir: vnc_png = Path(temp_dir) / "vnc.png" password = "123456" passwd_file = Path(temp_dir) / "pwd.txt" vncpasswd_generated = b"\x49\x40\x15\xf9\xa3\x5e\x8b\x22" passwd_file.write_bytes(vncpasswd_generated) if worker() == 0: with Display(backend="xvnc"): with api.connect("localhost:0") as client: client.timeout = 1 client.captureScreen(vnc_png) with XvncDisplay(): with api.connect("localhost:0") as client: client.timeout = 1 client.captureScreen(vnc_png) sconnect = "localhost:%s" % (rfbport() - 5900) with Display(backend="xvnc", rfbport=rfbport()): with api.connect(sconnect) as client: client.timeout = 1 client.captureScreen(vnc_png) with XvncDisplay(rfbport=rfbport()): with api.connect(sconnect) as client: client.timeout = 1 client.captureScreen(vnc_png) with Display(backend="xvnc", rfbport=rfbport(), rfbauth=passwd_file): with api.connect(sconnect, password=password) as client: client.timeout = 1 client.captureScreen(vnc_png) with XvncDisplay(rfbport=rfbport(), rfbauth=passwd_file): with api.connect(sconnect, password=password) as client: client.timeout = 1 client.captureScreen(vnc_png) PyVirtualDisplay-3.0/tests/tutil.py000066400000000000000000000022241420171065300175150ustar00rootroot00000000000000import logging import os import psutil from easyprocess import EasyProcess from pyvirtualdisplay.util import get_helptext log = logging.getLogger(__name__) def prog_check(cmd): try: p = EasyProcess(cmd) p.enable_stdout_log = False p.enable_stderr_log = False p.call() return p.return_code == 0 except Exception: return False # def platform_is_osx(): # return sys.platform == "darwin" def has_displayfd(): return "-displayfd" in get_helptext("Xvfb") def has_xvnc(): return prog_check(["Xvnc", "-help"]) def worker(): w = 0 PYTEST_XDIST_WORKER = os.environ.get("PYTEST_XDIST_WORKER") if PYTEST_XDIST_WORKER: # gw42 w = int(PYTEST_XDIST_WORKER[2:]) return w def rfbport(): port = 5900 + worker() + 9876 log.info("rfbport=%s", port) return port def kill_process_tree(ep): parent_pid = ep.pid parent = psutil.Process(parent_pid) for child in parent.children(recursive=True): try: child.kill() except psutil.NoSuchProcess: log.warning("NoSuchProcess error in kill_process_tree") parent.kill() PyVirtualDisplay-3.0/tests/vagrant/000077500000000000000000000000001420171065300174445ustar00rootroot00000000000000PyVirtualDisplay-3.0/tests/vagrant/Vagrantfile.debian10.rb000066400000000000000000000010401420171065300236100ustar00rootroot00000000000000Vagrant.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 PyVirtualDisplay-3.0/tests/vagrant/Vagrantfile.debian11.rb000066400000000000000000000010421420171065300236130ustar00rootroot00000000000000Vagrant.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 PyVirtualDisplay-3.0/tests/vagrant/Vagrantfile.ubuntu1804.rb000066400000000000000000000010301420171065300240630ustar00rootroot00000000000000Vagrant.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 PyVirtualDisplay-3.0/tests/vagrant/Vagrantfile.ubuntu2204.rb000066400000000000000000000011001420171065300240540ustar00rootroot00000000000000Vagrant.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 PyVirtualDisplay-3.0/tests/vagrant/debian10.sh000066400000000000000000000021751420171065300213700ustar00rootroot00000000000000#!/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 PyVirtualDisplay-3.0/tests/vagrant/debian11.sh000066400000000000000000000021751420171065300213710ustar00rootroot00000000000000#!/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 PyVirtualDisplay-3.0/tests/vagrant/osx.sh000077500000000000000000000021331420171065300206130ustar00rootroot00000000000000#!/bin/bash set -e #autologin brew tap xfreebird/utils brew install kcpassword enable_autologin "vagrant" "vagrant" # disable screensaver defaults -currentHost write com.apple.screensaver idleTime 0 # Turn Off System/Display/HDD Sleep sudo systemsetup -setcomputersleep Never sudo systemsetup -setdisplaysleep Never sudo systemsetup -setharddisksleep Never #https://github.com/ponty/PyVirtualDisplay/issues/42 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 # 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 # su - vagrant -c 'brew cask install xquartz' # su - vagrant -c 'python3 -m pip install --user pygame==2.0.0.dev6 pillow qtpy wxpython pyobjc-framework-Quartz pyobjc-framework-LaunchServices nose' sudo chown -R vagrant /vagrant PyVirtualDisplay-3.0/tests/vagrant/ubuntu1804.sh000066400000000000000000000021471420171065300216430ustar00rootroot00000000000000#!/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 PyVirtualDisplay-3.0/tests/vagrant/ubuntu2004.sh000066400000000000000000000021471420171065300216340ustar00rootroot00000000000000#!/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 PyVirtualDisplay-3.0/tests/vagrant/ubuntu2204.sh000066400000000000000000000021711420171065300216330ustar00rootroot00000000000000#!/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 PyVirtualDisplay-3.0/tests/vagrant/vagrant_boxes.py000077500000000000000000000070701420171065300226670ustar00rootroot00000000000000#!/usr/bin/env python3 import os from pathlib import Path from time import sleep import fabric from entrypoint2 import entrypoint import vagrant # pip3 install fabric vncdotool python-vagrant entrypoint2 DIR = Path(__file__).parent class Options: halt = True recreate = True destroy = False def run_box(options, vagrantfile, cmds): env = os.environ 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() state = status[0].state print(status) if options.destroy: v.halt(force=True) v.destroy() return if options.halt: v.halt() # avoid screensaver if state == "not_created": # install programs in box v.up() # restart box v.halt() try: v.up() with fabric.Connection( 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: if options.osx: freecmd = "top -l 1 -s 0 | grep PhysMem" else: # linux freecmd = "free -h" cmds = [freecmd, "env | sort"] + cmds + [freecmd] sleep(1) for cmd in cmds: if options.recreate: if "tox" in cmd: cmd += " -r" # hangs without pty=True conn.run(cmd, echo=True, pty=True) finally: if options.halt: v.halt() config = { "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'", # ], # ), } @entrypoint def main(boxes="all", fast=False, destroy=False): options = Options() options.halt = not fast options.recreate = not fast options.destroy = destroy if boxes == "all": boxes = list(config.keys()) else: boxes = boxes.split(",") for k, v in config.items(): 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("<-----") PyVirtualDisplay-3.0/tox.ini000066400000000000000000000016431420171065300161570ustar00rootroot00000000000000 [tox] envlist = py310 py39 py38 py37 py36 py310-doc py310-lint # Workaround for Vagrant #toxworkdir={toxinidir}/.tox # default toxworkdir={homedir}/.tox/pyvirtualdisplay [testenv] changedir=tests passenv=PYVIRTUALDISPLAY_DISPLAYFD #whitelist_externals = killall #commands_pre= # - killall Xvfb # - killall Xephyr # - killall Xvnc commands= {envpython} -m pytest -v . deps = -rrequirements-test.txt [testenv:py3-osx] changedir=tests passenv=PYVIRTUALDISPLAY_DISPLAYFD deps = -rrequirements-test.txt commands= {envpython} -m pytest -v . [testenv:py310-doc] allowlist_externals=bash changedir=doc 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"