From 862cdaaac564de9d578ef3ec400c5005ebe73254 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 15 Apr 2019 19:28:10 -0400 Subject: [PATCH 1/4] Auto-detect Xvfb on Linux if no X11 server found --- plotly/io/_orca.py | 68 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/plotly/io/_orca.py b/plotly/io/_orca.py index e8642d8c128..aa6f19a4f78 100644 --- a/plotly/io/_orca.py +++ b/plotly/io/_orca.py @@ -417,7 +417,9 @@ def executable(self): ------- str """ - return self._props.get('executable', 'orca') + return ' '.join(self._props.get( + 'executable_list', + ['orca'])) @executable.setter def executable(self, val): @@ -429,7 +431,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() @@ -738,7 +742,7 @@ class OrcaStatus(object): """ _props = { 'state': 'unvalidated', # or 'validated' or 'running' - 'executable': None, + 'executable_list': None, 'version': None, 'pid': None, 'port': None, @@ -770,7 +774,7 @@ def executable(self): This property will be None if the `state` is 'unvalidated'. """ - return self._props['executable'] + return ' '.join(self._props['executable_list']) @property def version(self): @@ -851,7 +855,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: @@ -949,6 +957,24 @@ def validate_executable(): formatted_path=formatted_path, instructions=install_location_instructions)) + executable_list = [executable] + + # Check if we should run with Xvfb + # -------------------------------- + if (sys.platform.startswith('linux') and + not os.environ.get('DISPLAY')): + # We're on linux without a display server. See if Xvfb is available + xvfb_run_executable = which('xvfb-run') + + if xvfb_run_executable: + executable_list = [ + xvfb_run_executable, + "--auto-servernum", + "--server-args", + "-screen 0 640x480x24 +extension RANDR +extension GLX", + executable + ] + # Run executable with --help and see if it's our orca # --------------------------------------------------- invalid_executable_msg = """ @@ -964,7 +990,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) @@ -977,7 +1003,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) @@ -997,7 +1023,7 @@ def validate_executable(): 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')): @@ -1006,14 +1032,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) @@ -1029,7 +1055,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)) @@ -1039,11 +1065,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' @@ -1061,7 +1087,7 @@ def reset_status(): None """ shutdown_server() - status._props['executable'] = None + status._props['executable_list'] = None status._props['version'] = None status._props['state'] = 'unvalidated' @@ -1179,10 +1205,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]) @@ -1198,8 +1225,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 From c837b2d7e6143163813d994364f9d986cc4c195f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 15 Apr 2019 19:43:11 -0400 Subject: [PATCH 2/4] Fix executable_list is None --- plotly/io/_orca.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plotly/io/_orca.py b/plotly/io/_orca.py index aa6f19a4f78..f1077e03c42 100644 --- a/plotly/io/_orca.py +++ b/plotly/io/_orca.py @@ -417,9 +417,11 @@ def executable(self): ------- str """ - return ' '.join(self._props.get( - 'executable_list', - ['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): @@ -774,7 +776,11 @@ def executable(self): This property will be None if the `state` is 'unvalidated'. """ - return ' '.join(self._props['executable_list']) + executable_list = self._props['executable_list'] + if executable_list is None: + return None + else: + return ' '.join(executable_list) @property def version(self): From 0a0ffa5994ba47a9f35fcc86ff2c3db73f90d2b4 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 23 May 2019 19:27:51 -0400 Subject: [PATCH 3/4] 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. --- _plotly_future_/orca_defaults.py | 5 +++ _plotly_future_/v4.py | 2 +- plotly/io/_orca.py | 74 ++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 _plotly_future_/orca_defaults.py diff --git a/_plotly_future_/orca_defaults.py b/_plotly_future_/orca_defaults.py new file mode 100644 index 00000000000..b63282fb6a1 --- /dev/null +++ b/_plotly_future_/orca_defaults.py @@ -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') diff --git a/_plotly_future_/v4.py b/_plotly_future_/v4.py index 7e5c023a720..ff82e827be2 100644 --- a/_plotly_future_/v4.py +++ b/_plotly_future_/v4.py @@ -1,4 +1,4 @@ from __future__ import absolute_import from _plotly_future_ import ( renderer_defaults, template_defaults, extract_chart_studio, - remove_deprecations) + remove_deprecations, orca_defaults) diff --git a/plotly/io/_orca.py b/plotly/io/_orca.py index f1077e03c42..6e164452ef2 100644 --- a/plotly/io/_orca.py +++ b/plotly/io/_orca.py @@ -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') @@ -667,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): """ @@ -710,6 +734,7 @@ def __repr__(self): mathjax: {mathjax} topojson: {topojson} mapbox_access_token: {mapbox_access_token} + use_xvfb: {use_xvfb} constants --------- @@ -727,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 @@ -946,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. @@ -963,23 +988,36 @@ def validate_executable(): formatted_path=formatted_path, instructions=install_location_instructions)) - executable_list = [executable] - # Check if we should run with Xvfb # -------------------------------- - if (sys.platform.startswith('linux') and - not os.environ.get('DISPLAY')): - # We're on linux without a display server. See if Xvfb is available - xvfb_run_executable = which('xvfb-run') + xvfb_args = ["--auto-servernum", + "--server-args", + "-screen 0 640x480x24 +extension RANDR +extension GLX", + executable] - if xvfb_run_executable: - executable_list = [ - xvfb_run_executable, - "--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 # --------------------------------------------------- From 4bf5355b5344634d638eb7b74878ae0994234185 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 25 May 2019 09:22:18 -0400 Subject: [PATCH 4/4] Update X11 headless error message --- plotly/io/_orca.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plotly/io/_orca.py b/plotly/io/_orca.py index 6e164452ef2..19bb51b432b 100644 --- a/plotly/io/_orca.py +++ b/plotly/io/_orca.py @@ -1057,9 +1057,17 @@ 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)