Skip to content

Commit 6e19297

Browse files
authored
Merge pull request #351 from seleniumbase/custom-virtual-display
Use a customized virtual display in headless mode
2 parents a8411a8 + 73bd10d commit 6e19297

File tree

15 files changed

+874
-10
lines changed

15 files changed

+874
-10
lines changed

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,3 @@ pyotp>=2.3.0
2929
boto>=2.49.0
3030
flake8>=3.7.8
3131
certifi>=2019.6.16
32-
PyVirtualDisplay==0.2.1

seleniumbase/common/unobfuscate.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
"""
1010

1111
from seleniumbase.common import encryption
12+
import sys
1213
import time
1314

1415

1516
def main():
16-
try:
17+
if sys.version_info[0] >= 3:
18+
input_method = input # Using Python 3 (or higher)
19+
else:
1720
# Python 2 has the raw_input() method. Python 3 does not.
1821
input_method = raw_input # noqa: ignore=F821
19-
except Exception:
20-
input_method = input # Using Python 3
2122
try:
2223
while(1):
2324
code = input_method(

seleniumbase/core/log_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def log_test_failure_data(test, test_logpath, driver, browser):
3333
data_to_save = []
3434
data_to_save.append("Last_Page: %s" % last_page)
3535
data_to_save.append("Browser: %s " % browser)
36-
if sys.version.startswith('3') and hasattr(test, '_outcome'):
36+
if sys.version_info[0] >= 3 and hasattr(test, '_outcome'):
3737
if test._outcome.errors:
3838
try:
3939
exc_message = test._outcome.errors[0][1][1]

seleniumbase/fixtures/base_case.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,7 +3256,8 @@ def setUp(self, masterqa_mode=False):
32563256
self.case_start_time = int(time.time() * 1000)
32573257
if self.headless:
32583258
try:
3259-
from pyvirtualdisplay import Display
3259+
# from pyvirtualdisplay import Display # Skip for own lib
3260+
from seleniumbase.virtual_display.display import Display
32603261
self.display = Display(visible=0, size=(1440, 1880))
32613262
self.display.start()
32623263
self.headless_active = True
@@ -3376,7 +3377,7 @@ def tearDown(self):
33763377
super(SubClassOfBaseCase, self).tearDown()
33773378
"""
33783379
has_exception = False
3379-
if sys.version.startswith('3') and hasattr(self, '_outcome'):
3380+
if sys.version_info[0] >= 3 and hasattr(self, '_outcome'):
33803381
if hasattr(self._outcome, 'errors') and self._outcome.errors:
33813382
has_exception = True
33823383
else:

seleniumbase/plugins/selenium_plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ def beforeTest(self, test):
285285
test.test.headed = True
286286
if self.options.headless:
287287
try:
288-
from pyvirtualdisplay import Display
288+
# from pyvirtualdisplay import Display # Skip for own lib
289+
from seleniumbase.virtual_display.display import Display
289290
self.display = Display(visible=0, size=(1440, 1880))
290291
self.display.start()
291292
self.headless_active = True

seleniumbase/virtual_display/__init__.py

Whitespace-only changes.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import fnmatch
2+
import os
3+
import tempfile
4+
import time
5+
from threading import Lock
6+
from seleniumbase.virtual_display.easyprocess import EasyProcess
7+
from seleniumbase.virtual_display import xauth
8+
9+
mutex = Lock()
10+
RANDOMIZE_DISPLAY_NR = False
11+
if RANDOMIZE_DISPLAY_NR:
12+
import random
13+
random.seed()
14+
MIN_DISPLAY_NR = 1000
15+
USED_DISPLAY_NR_LIST = []
16+
17+
18+
class AbstractDisplay(EasyProcess):
19+
'''
20+
Common parent for Xvfb and Xephyr
21+
'''
22+
def __init__(self, use_xauth=False):
23+
mutex.acquire()
24+
try:
25+
self.display = self.search_for_display()
26+
while self.display in USED_DISPLAY_NR_LIST:
27+
self.display += 1
28+
USED_DISPLAY_NR_LIST.append(self.display)
29+
finally:
30+
mutex.release()
31+
if xauth and not xauth.is_installed():
32+
raise xauth.NotFoundError()
33+
self.use_xauth = use_xauth
34+
self._old_xauth = None
35+
self._xauth_filename = None
36+
EasyProcess.__init__(self, self._cmd)
37+
38+
@property
39+
def new_display_var(self):
40+
return ':%s' % (self.display)
41+
42+
@property
43+
def _cmd(self):
44+
raise NotImplementedError()
45+
46+
def lock_files(self):
47+
tmpdir = '/tmp'
48+
pattern = '.X*-lock'
49+
# remove path.py dependency
50+
names = fnmatch.filter(os.listdir(tmpdir), pattern)
51+
ls = [os.path.join(tmpdir, child) for child in names]
52+
ls = [p for p in ls if os.path.isfile(p)]
53+
return ls
54+
55+
def search_for_display(self):
56+
# search for free display
57+
ls = [int(x.split('X')[1].split('-')[0]) for x in self.lock_files()]
58+
if len(ls):
59+
display = max(MIN_DISPLAY_NR, max(ls) + 3)
60+
else:
61+
display = MIN_DISPLAY_NR
62+
if RANDOMIZE_DISPLAY_NR:
63+
display += random.randint(0, 100)
64+
return display
65+
66+
def redirect_display(self, on):
67+
'''
68+
on:
69+
* True -> set $DISPLAY to virtual screen
70+
* False -> set $DISPLAY to original screen
71+
72+
:param on: bool
73+
'''
74+
d = self.new_display_var if on else self.old_display_var
75+
if d is None:
76+
del os.environ['DISPLAY']
77+
else:
78+
os.environ['DISPLAY'] = d
79+
80+
def start(self):
81+
'''
82+
start display
83+
84+
:rtype: self
85+
'''
86+
if self.use_xauth:
87+
self._setup_xauth()
88+
EasyProcess.start(self)
89+
90+
# https://github.com/ponty/PyVirtualDisplay/issues/2
91+
# https://github.com/ponty/PyVirtualDisplay/issues/14
92+
self.old_display_var = os.environ.get('DISPLAY', None)
93+
94+
self.redirect_display(True)
95+
# wait until X server is active
96+
# TODO: better method
97+
time.sleep(0.1)
98+
return self
99+
100+
def stop(self):
101+
'''
102+
stop display
103+
104+
:rtype: self
105+
'''
106+
self.redirect_display(False)
107+
EasyProcess.stop(self)
108+
if self.use_xauth:
109+
self._clear_xauth()
110+
return self
111+
112+
def _setup_xauth(self):
113+
'''
114+
Set up the Xauthority file and the XAUTHORITY environment variable.
115+
'''
116+
handle, filename = tempfile.mkstemp(prefix='PyVirtualDisplay.',
117+
suffix='.Xauthority')
118+
self._xauth_filename = filename
119+
os.close(handle)
120+
# Save old environment
121+
self._old_xauth = {}
122+
self._old_xauth['AUTHFILE'] = os.getenv('AUTHFILE')
123+
self._old_xauth['XAUTHORITY'] = os.getenv('XAUTHORITY')
124+
125+
os.environ['AUTHFILE'] = os.environ['XAUTHORITY'] = filename
126+
cookie = xauth.generate_mcookie()
127+
xauth.call('add', self.new_display_var, '.', cookie)
128+
129+
def _clear_xauth(self):
130+
'''
131+
Clear the Xauthority file and restore the environment variables.
132+
'''
133+
os.remove(self._xauth_filename)
134+
for varname in ['AUTHFILE', 'XAUTHORITY']:
135+
if self._old_xauth[varname] is None:
136+
del os.environ[varname]
137+
else:
138+
os.environ[varname] = self._old_xauth[varname]
139+
self._old_xauth = None
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
This module contains a customized version of pyvirtualdisplay.
3+
These helper methods SHOULD NOT be called directly from tests.
4+
"""
5+
from seleniumbase.virtual_display.abstractdisplay import AbstractDisplay
6+
from seleniumbase.virtual_display.xephyr import XephyrDisplay
7+
from seleniumbase.virtual_display.xvfb import XvfbDisplay
8+
from seleniumbase.virtual_display.xvnc import XvncDisplay
9+
10+
11+
class Display(AbstractDisplay):
12+
'''
13+
Common class
14+
15+
:param color_depth: [8, 16, 24, 32]
16+
:param size: screen size (width,height)
17+
:param bgcolor: background color ['black' or 'white']
18+
:param visible: True -> Xephyr, False -> Xvfb
19+
:param backend: 'xvfb', 'xvnc' or 'xephyr', ignores ``visible``
20+
:param xauth: If a Xauthority file should be created.
21+
'''
22+
def __init__(self, backend=None, visible=False, size=(1024, 768),
23+
color_depth=24, bgcolor='black', use_xauth=False, **kwargs):
24+
self.color_depth = color_depth
25+
self.size = size
26+
self.bgcolor = bgcolor
27+
self.screen = 0
28+
self.process = None
29+
self.display = None
30+
self.visible = visible
31+
self.backend = backend
32+
33+
if not self.backend:
34+
if self.visible:
35+
self.backend = 'xephyr'
36+
else:
37+
self.backend = 'xvfb'
38+
39+
self._obj = self.display_class(
40+
size=size,
41+
color_depth=color_depth,
42+
bgcolor=bgcolor,
43+
**kwargs)
44+
AbstractDisplay.__init__(self, use_xauth=use_xauth)
45+
46+
@property
47+
def display_class(self):
48+
assert self.backend
49+
if self.backend == 'xvfb':
50+
cls = XvfbDisplay
51+
if self.backend == 'xvnc':
52+
cls = XvncDisplay
53+
if self.backend == 'xephyr':
54+
cls = XephyrDisplay
55+
cls.check_installed()
56+
return cls
57+
58+
@property
59+
def _cmd(self):
60+
self._obj.display = self.display
61+
return self._obj._cmd

0 commit comments

Comments
 (0)