Skip to content

Support calling orca through xvfb on Linux #1523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions _plotly_future_/orca_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from __future__ import absolute_import
from _plotly_future_ import _future_flags, _assert_plotly_not_imported

_assert_plotly_not_imported()
_future_flags.add('orca_defaults')
10 changes: 8 additions & 2 deletions _plotly_future_/v4.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from __future__ import absolute_import
from _plotly_future_ import (
renderer_defaults, template_defaults, extract_chart_studio,
remove_deprecations, v4_subplots)
renderer_defaults,
template_defaults,
extract_chart_studio,
remove_deprecations,
v4_subplots,
orca_defaults,
)

134 changes: 107 additions & 27 deletions plotly/io/_orca.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

psutil = get_module('psutil')

from _plotly_future_ import _future_flags

# Valid image format constants
# ----------------------------
valid_formats = ('png', 'jpeg', 'webp', 'svg', 'pdf', 'eps')
Expand Down Expand Up @@ -417,7 +419,11 @@ def executable(self):
-------
str
"""
return self._props.get('executable', 'orca')
executable_list = self._props.get('executable_list', ['orca'])
if executable_list is None:
return None
else:
return ' '.join(executable_list)

@executable.setter
def executable(self, val):
Expand All @@ -429,7 +435,9 @@ def executable(self, val):
raise ValueError("""
The executable property must be a string, but received value of type {typ}.
Received value: {val}""".format(typ=type(val), val=val))
self._props['executable'] = val
if isinstance(val, string_types):
val = [val]
self._props['executable_list'] = val

# Server and validation must restart before setting is active
reset_status()
Expand Down Expand Up @@ -661,6 +669,28 @@ def mapbox_access_token(self, val):
# Server must restart before setting is active
shutdown_server()

@property
def use_xvfb(self):
dflt = 'auto' if 'orca_defaults' in _future_flags else False
return self._props.get('use_xvfb', dflt)

@use_xvfb.setter
def use_xvfb(self, val):
valid_vals = [True, False, 'auto']
if val is None:
self._props.pop('use_xvfb', None)
else:
if val not in valid_vals:
raise ValueError("""
The use_xvfb property must be one of {valid_vals}
Received value of type {typ}: {val}""".format(
valid_vals=valid_vals, typ=type(val), val=repr(val)))

self._props['use_xvfb'] = val

# Server and validation must restart before setting is active
reset_status()

@property
def plotlyjs(self):
"""
Expand Down Expand Up @@ -704,6 +734,7 @@ def __repr__(self):
mathjax: {mathjax}
topojson: {topojson}
mapbox_access_token: {mapbox_access_token}
use_xvfb: {use_xvfb}

constants
---------
Expand All @@ -721,7 +752,8 @@ def __repr__(self):
topojson=self.topojson,
mapbox_access_token=self.mapbox_access_token,
plotlyjs=self.plotlyjs,
config_file=self.config_file)
config_file=self.config_file,
use_xvfb=self.use_xvfb)


# Make config a singleton object
Expand All @@ -738,7 +770,7 @@ class OrcaStatus(object):
"""
_props = {
'state': 'unvalidated', # or 'validated' or 'running'
'executable': None,
'executable_list': None,
'version': None,
'pid': None,
'port': None,
Expand Down Expand Up @@ -770,7 +802,11 @@ def executable(self):

This property will be None if the `state` is 'unvalidated'.
"""
return self._props['executable']
executable_list = self._props['executable_list']
if executable_list is None:
return None
else:
return ' '.join(executable_list)

@property
def version(self):
Expand Down Expand Up @@ -851,7 +887,11 @@ def orca_env():
to orca is transformed into a call to nodejs.
See https://github.com/plotly/orca/issues/149#issuecomment-443506732
"""
clear_env_vars = ['NODE_OPTIONS', 'ELECTRON_RUN_AS_NODE']
clear_env_vars = [
'NODE_OPTIONS',
'ELECTRON_RUN_AS_NODE',
'LD_PRELOAD'
]
orig_env_vars = {}

try:
Expand Down Expand Up @@ -932,11 +972,10 @@ def validate_executable():
# -------------------------
# Search for executable name or path in config.executable
executable = which(config.executable)
path = os.environ.get("PATH", os.defpath)
formatted_path = path.replace(os.pathsep, '\n ')

if executable is None:
path = os.environ.get("PATH", os.defpath)
formatted_path = path.replace(os.pathsep, '\n ')

raise ValueError("""
The orca executable is required to export figures as static images,
but it could not be found on the system path.
Expand All @@ -949,6 +988,37 @@ def validate_executable():
formatted_path=formatted_path,
instructions=install_location_instructions))

# Check if we should run with Xvfb
# --------------------------------
xvfb_args = ["--auto-servernum",
"--server-args",
"-screen 0 640x480x24 +extension RANDR +extension GLX",
executable]

if config.use_xvfb == True:
# Use xvfb
xvfb_run_executable = which('xvfb-run')
if not xvfb_run_executable:
raise ValueError("""
The plotly.io.orca.config.use_xvfb property is set to True, but the
xvfb-run executable could not be found on the system path.

Searched for the executable 'xvfb-run' on the following path:
{formatted_path}""".format(formatted_path=formatted_path))

executable_list = [xvfb_run_executable] + xvfb_args
elif (config.use_xvfb == 'auto' and
sys.platform.startswith('linux') and
not os.environ.get('DISPLAY') and
which('xvfb-run')):
# use_xvfb is 'auto', we're on linux without a display server,
# and xvfb-run is available. Use it.
xvfb_run_executable = which('xvfb-run')
executable_list = [xvfb_run_executable] + xvfb_args
else:
# Do not use xvfb
executable_list = [executable]

# Run executable with --help and see if it's our orca
# ---------------------------------------------------
invalid_executable_msg = """
Expand All @@ -964,7 +1034,7 @@ def validate_executable():
# ### Run with Popen so we get access to stdout and stderr
with orca_env():
p = subprocess.Popen(
[executable, '--help'],
executable_list + ['--help'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

Expand All @@ -977,7 +1047,7 @@ def validate_executable():

[Return code: {returncode}]
{err_msg}
""".format(executable=executable,
""".format(executable=' '.join(executable_list),
err_msg=help_error.decode('utf-8'),
returncode=p.returncode)

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

err_msg += """\
Note: When used on Linux, orca requires an X11 display server, but none was
detected. Please install X11, or configure your system with Xvfb. See
the orca README (https://github.com/plotly/orca) for instructions on using
orca with Xvfb.
detected. Please install Xvfb and configure plotly.py to run orca using Xvfb
as follows:

>>> import plotly.io as pio
>>> pio.orca.config.use_xvfb = True

You can save this configuration for use in future sessions as follows:
>>> pio.orca.config.save()

See https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml
for more info on Xvfb
"""
raise ValueError(err_msg)

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

if ("Plotly's image-exporting utilities" not in
help_result.decode('utf-8')):
Expand All @@ -1006,14 +1084,14 @@ def validate_executable():
$ {executable} --help

{help_result}
""".format(executable=executable, help_result=help_result))
""".format(executable=' '.join(executable_list), help_result=help_result))

# Get orca version
# ----------------
# ### Run with Popen so we get access to stdout and stderr
with orca_env():
p = subprocess.Popen(
[executable, '--version'],
executable_list + ['--version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

Expand All @@ -1029,7 +1107,7 @@ def validate_executable():

[Return code: {returncode}]
{err_msg}
""".format(executable=executable,
""".format(executable=' '.join(executable_list),
err_msg=version_error.decode('utf-8'),
returncode=p.returncode))

Expand All @@ -1039,11 +1117,11 @@ def validate_executable():
Here is the command that plotly.py ran to request the version:

$ {executable} --version
""".format(executable=executable))
""".format(executable=' '.join(executable_list)))
else:
version_result = version_result.decode()

status._props['executable'] = executable
status._props['executable_list'] = executable_list
status._props['version'] = version_result.strip()
status._props['state'] = 'validated'

Expand All @@ -1061,7 +1139,7 @@ def reset_status():
None
"""
shutdown_server()
status._props['executable'] = None
status._props['executable_list'] = None
status._props['version'] = None
status._props['state'] = 'unvalidated'

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

# Build orca command list
cmd_list = [status.executable, 'serve',
'-p', str(orca_state['port']),
'--plotly', config.plotlyjs,
'--graph-only']
cmd_list = status._props['executable_list'] + [
'serve',
'-p', str(orca_state['port']),
'--plotly', config.plotlyjs,
'--graph-only']

if config.topojson:
cmd_list.extend(['--topojson', config.topojson])
Expand All @@ -1198,8 +1277,9 @@ def ensure_server():
# specified port.
DEVNULL = open(os.devnull, 'wb')
with orca_env():
orca_state['proc'] = subprocess.Popen(cmd_list,
stdout=DEVNULL)
orca_state['proc'] = subprocess.Popen(
cmd_list, stdout=DEVNULL
)

# Update orca.status so the user has an accurate view
# of the state of the orca server
Expand Down