Skip to content

Commit 3f890a5

Browse files
authored
Support calling orca through xvfb on Linux (#1523)
Add orca.config.use_xvfb property to control whether xvfb-run should be used to run orca. May by True, False, or 'auto'. The default will be False until v4 (or when the orca_defaults future flag is enabled). In v4 the default will change to 'auto', in which case xvfb-run is used automatically if plotly.py is running on a Linux machine without a display server with xvfb-run available.
1 parent 0c5665f commit 3f890a5

File tree

3 files changed

+120
-29
lines changed

3 files changed

+120
-29
lines changed

_plotly_future_/orca_defaults.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import absolute_import
2+
from _plotly_future_ import _future_flags, _assert_plotly_not_imported
3+
4+
_assert_plotly_not_imported()
5+
_future_flags.add('orca_defaults')

_plotly_future_/v4.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
from __future__ import absolute_import
22
from _plotly_future_ import (
3-
renderer_defaults, template_defaults, extract_chart_studio,
4-
remove_deprecations, v4_subplots)
3+
renderer_defaults,
4+
template_defaults,
5+
extract_chart_studio,
6+
remove_deprecations,
7+
v4_subplots,
8+
orca_defaults,
9+
)
10+

plotly/io/_orca.py

+107-27
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
psutil = get_module('psutil')
2424

25+
from _plotly_future_ import _future_flags
26+
2527
# Valid image format constants
2628
# ----------------------------
2729
valid_formats = ('png', 'jpeg', 'webp', 'svg', 'pdf', 'eps')
@@ -417,7 +419,11 @@ def executable(self):
417419
-------
418420
str
419421
"""
420-
return self._props.get('executable', 'orca')
422+
executable_list = self._props.get('executable_list', ['orca'])
423+
if executable_list is None:
424+
return None
425+
else:
426+
return ' '.join(executable_list)
421427

422428
@executable.setter
423429
def executable(self, val):
@@ -429,7 +435,9 @@ def executable(self, val):
429435
raise ValueError("""
430436
The executable property must be a string, but received value of type {typ}.
431437
Received value: {val}""".format(typ=type(val), val=val))
432-
self._props['executable'] = val
438+
if isinstance(val, string_types):
439+
val = [val]
440+
self._props['executable_list'] = val
433441

434442
# Server and validation must restart before setting is active
435443
reset_status()
@@ -661,6 +669,28 @@ def mapbox_access_token(self, val):
661669
# Server must restart before setting is active
662670
shutdown_server()
663671

672+
@property
673+
def use_xvfb(self):
674+
dflt = 'auto' if 'orca_defaults' in _future_flags else False
675+
return self._props.get('use_xvfb', dflt)
676+
677+
@use_xvfb.setter
678+
def use_xvfb(self, val):
679+
valid_vals = [True, False, 'auto']
680+
if val is None:
681+
self._props.pop('use_xvfb', None)
682+
else:
683+
if val not in valid_vals:
684+
raise ValueError("""
685+
The use_xvfb property must be one of {valid_vals}
686+
Received value of type {typ}: {val}""".format(
687+
valid_vals=valid_vals, typ=type(val), val=repr(val)))
688+
689+
self._props['use_xvfb'] = val
690+
691+
# Server and validation must restart before setting is active
692+
reset_status()
693+
664694
@property
665695
def plotlyjs(self):
666696
"""
@@ -704,6 +734,7 @@ def __repr__(self):
704734
mathjax: {mathjax}
705735
topojson: {topojson}
706736
mapbox_access_token: {mapbox_access_token}
737+
use_xvfb: {use_xvfb}
707738
708739
constants
709740
---------
@@ -721,7 +752,8 @@ def __repr__(self):
721752
topojson=self.topojson,
722753
mapbox_access_token=self.mapbox_access_token,
723754
plotlyjs=self.plotlyjs,
724-
config_file=self.config_file)
755+
config_file=self.config_file,
756+
use_xvfb=self.use_xvfb)
725757

726758

727759
# Make config a singleton object
@@ -738,7 +770,7 @@ class OrcaStatus(object):
738770
"""
739771
_props = {
740772
'state': 'unvalidated', # or 'validated' or 'running'
741-
'executable': None,
773+
'executable_list': None,
742774
'version': None,
743775
'pid': None,
744776
'port': None,
@@ -770,7 +802,11 @@ def executable(self):
770802
771803
This property will be None if the `state` is 'unvalidated'.
772804
"""
773-
return self._props['executable']
805+
executable_list = self._props['executable_list']
806+
if executable_list is None:
807+
return None
808+
else:
809+
return ' '.join(executable_list)
774810

775811
@property
776812
def version(self):
@@ -851,7 +887,11 @@ def orca_env():
851887
to orca is transformed into a call to nodejs.
852888
See https://github.com/plotly/orca/issues/149#issuecomment-443506732
853889
"""
854-
clear_env_vars = ['NODE_OPTIONS', 'ELECTRON_RUN_AS_NODE']
890+
clear_env_vars = [
891+
'NODE_OPTIONS',
892+
'ELECTRON_RUN_AS_NODE',
893+
'LD_PRELOAD'
894+
]
855895
orig_env_vars = {}
856896

857897
try:
@@ -932,11 +972,10 @@ def validate_executable():
932972
# -------------------------
933973
# Search for executable name or path in config.executable
934974
executable = which(config.executable)
975+
path = os.environ.get("PATH", os.defpath)
976+
formatted_path = path.replace(os.pathsep, '\n ')
935977

936978
if executable is None:
937-
path = os.environ.get("PATH", os.defpath)
938-
formatted_path = path.replace(os.pathsep, '\n ')
939-
940979
raise ValueError("""
941980
The orca executable is required to export figures as static images,
942981
but it could not be found on the system path.
@@ -949,6 +988,37 @@ def validate_executable():
949988
formatted_path=formatted_path,
950989
instructions=install_location_instructions))
951990

991+
# Check if we should run with Xvfb
992+
# --------------------------------
993+
xvfb_args = ["--auto-servernum",
994+
"--server-args",
995+
"-screen 0 640x480x24 +extension RANDR +extension GLX",
996+
executable]
997+
998+
if config.use_xvfb == True:
999+
# Use xvfb
1000+
xvfb_run_executable = which('xvfb-run')
1001+
if not xvfb_run_executable:
1002+
raise ValueError("""
1003+
The plotly.io.orca.config.use_xvfb property is set to True, but the
1004+
xvfb-run executable could not be found on the system path.
1005+
1006+
Searched for the executable 'xvfb-run' on the following path:
1007+
{formatted_path}""".format(formatted_path=formatted_path))
1008+
1009+
executable_list = [xvfb_run_executable] + xvfb_args
1010+
elif (config.use_xvfb == 'auto' and
1011+
sys.platform.startswith('linux') and
1012+
not os.environ.get('DISPLAY') and
1013+
which('xvfb-run')):
1014+
# use_xvfb is 'auto', we're on linux without a display server,
1015+
# and xvfb-run is available. Use it.
1016+
xvfb_run_executable = which('xvfb-run')
1017+
executable_list = [xvfb_run_executable] + xvfb_args
1018+
else:
1019+
# Do not use xvfb
1020+
executable_list = [executable]
1021+
9521022
# Run executable with --help and see if it's our orca
9531023
# ---------------------------------------------------
9541024
invalid_executable_msg = """
@@ -964,7 +1034,7 @@ def validate_executable():
9641034
# ### Run with Popen so we get access to stdout and stderr
9651035
with orca_env():
9661036
p = subprocess.Popen(
967-
[executable, '--help'],
1037+
executable_list + ['--help'],
9681038
stdout=subprocess.PIPE,
9691039
stderr=subprocess.PIPE)
9701040

@@ -977,7 +1047,7 @@ def validate_executable():
9771047
9781048
[Return code: {returncode}]
9791049
{err_msg}
980-
""".format(executable=executable,
1050+
""".format(executable=' '.join(executable_list),
9811051
err_msg=help_error.decode('utf-8'),
9821052
returncode=p.returncode)
9831053

@@ -987,17 +1057,25 @@ def validate_executable():
9871057

9881058
err_msg += """\
9891059
Note: When used on Linux, orca requires an X11 display server, but none was
990-
detected. Please install X11, or configure your system with Xvfb. See
991-
the orca README (https://github.com/plotly/orca) for instructions on using
992-
orca with Xvfb.
1060+
detected. Please install Xvfb and configure plotly.py to run orca using Xvfb
1061+
as follows:
1062+
1063+
>>> import plotly.io as pio
1064+
>>> pio.orca.config.use_xvfb = True
1065+
1066+
You can save this configuration for use in future sessions as follows:
1067+
>>> pio.orca.config.save()
1068+
1069+
See https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml
1070+
for more info on Xvfb
9931071
"""
9941072
raise ValueError(err_msg)
9951073

9961074
if not help_result:
9971075
raise ValueError(invalid_executable_msg + """
9981076
The error encountered is that no output was returned by the command
9991077
$ {executable} --help
1000-
""".format(executable=executable))
1078+
""".format(executable=' '.join(executable_list)))
10011079

10021080
if ("Plotly's image-exporting utilities" not in
10031081
help_result.decode('utf-8')):
@@ -1006,14 +1084,14 @@ def validate_executable():
10061084
$ {executable} --help
10071085
10081086
{help_result}
1009-
""".format(executable=executable, help_result=help_result))
1087+
""".format(executable=' '.join(executable_list), help_result=help_result))
10101088

10111089
# Get orca version
10121090
# ----------------
10131091
# ### Run with Popen so we get access to stdout and stderr
10141092
with orca_env():
10151093
p = subprocess.Popen(
1016-
[executable, '--version'],
1094+
executable_list + ['--version'],
10171095
stdout=subprocess.PIPE,
10181096
stderr=subprocess.PIPE)
10191097

@@ -1029,7 +1107,7 @@ def validate_executable():
10291107
10301108
[Return code: {returncode}]
10311109
{err_msg}
1032-
""".format(executable=executable,
1110+
""".format(executable=' '.join(executable_list),
10331111
err_msg=version_error.decode('utf-8'),
10341112
returncode=p.returncode))
10351113

@@ -1039,11 +1117,11 @@ def validate_executable():
10391117
Here is the command that plotly.py ran to request the version:
10401118
10411119
$ {executable} --version
1042-
""".format(executable=executable))
1120+
""".format(executable=' '.join(executable_list)))
10431121
else:
10441122
version_result = version_result.decode()
10451123

1046-
status._props['executable'] = executable
1124+
status._props['executable_list'] = executable_list
10471125
status._props['version'] = version_result.strip()
10481126
status._props['state'] = 'validated'
10491127

@@ -1061,7 +1139,7 @@ def reset_status():
10611139
None
10621140
"""
10631141
shutdown_server()
1064-
status._props['executable'] = None
1142+
status._props['executable_list'] = None
10651143
status._props['version'] = None
10661144
status._props['state'] = 'unvalidated'
10671145

@@ -1179,10 +1257,11 @@ def ensure_server():
11791257
orca_state['port'] = config.port
11801258

11811259
# Build orca command list
1182-
cmd_list = [status.executable, 'serve',
1183-
'-p', str(orca_state['port']),
1184-
'--plotly', config.plotlyjs,
1185-
'--graph-only']
1260+
cmd_list = status._props['executable_list'] + [
1261+
'serve',
1262+
'-p', str(orca_state['port']),
1263+
'--plotly', config.plotlyjs,
1264+
'--graph-only']
11861265

11871266
if config.topojson:
11881267
cmd_list.extend(['--topojson', config.topojson])
@@ -1198,8 +1277,9 @@ def ensure_server():
11981277
# specified port.
11991278
DEVNULL = open(os.devnull, 'wb')
12001279
with orca_env():
1201-
orca_state['proc'] = subprocess.Popen(cmd_list,
1202-
stdout=DEVNULL)
1280+
orca_state['proc'] = subprocess.Popen(
1281+
cmd_list, stdout=DEVNULL
1282+
)
12031283

12041284
# Update orca.status so the user has an accurate view
12051285
# of the state of the orca server

0 commit comments

Comments
 (0)